This doc will cover some applications of simple and complex models to our Alzheimer’s dataset, and talk about how the results could be applied or interpereted much like the work originally done in the paper “Multiplexed immunoassay panel identifies novel CSF biomarkers for Alzheimer’s disease diagnosis and prognosis”.

Some of the topics covered:

Refresher on Our AD Data

Load in the same data as before and run some basic analysis to get back in the munging mood:

# Load in the whole Alzheimers data file
data <- read.csv('~/repos/portfolio/demonstrative/R/datasets/alzheimers/alzheimers.csv')
# Remove this field ... I forgot to do that when creating the dataset
data <- data %>% select(-male)
# Normalize gender labels
stopifnot(all(!is.na(data$gender)))
normalize.gender <- function(x) {
  is.male <- x %>% tolower %>% str_detect('^m')
  ifelse(is.male, 'Male', 'Female') %>% factor
}
data <- data %>% mutate(gender=normalize.gender(gender))
# Convert integer fields to numeric for the sake of consistency
data <- data %>% mutate_each_(funs(as.numeric), c('Betacellulin', 'Eotaxin_3'))
head(data)
# Parse our data into predictors and a binary response 
# (Impaired vs Not Impaired) and check response frequency
X <- data %>% select(-response)
y <- data[,'response']
table(y)
y
   Impaired NotImpaired 
         91         242 
table(y) / length(y)
y
   Impaired NotImpaired 
  0.2732733   0.7267267 
# Check out the names of the predictors we have:
names(X)
  [1] "ACE_CD143_Angiotensin_Converti"   "ACTH_Adrenocorticotropic_Hormon" 
  [3] "AXL"                              "Adiponectin"                     
  [5] "Alpha_1_Antichymotrypsin"         "Alpha_1_Antitrypsin"             
  [7] "Alpha_1_Microglobulin"            "Alpha_2_Macroglobulin"           
  [9] "Angiopoietin_2_ANG_2"             "Angiotensinogen"                 
 [11] "Apolipoprotein_A_IV"              "Apolipoprotein_A1"               
 [13] "Apolipoprotein_A2"                "Apolipoprotein_B"                
 [15] "Apolipoprotein_CI"                "Apolipoprotein_CIII"             
 [17] "Apolipoprotein_D"                 "Apolipoprotein_E"                
 [19] "Apolipoprotein_H"                 "B_Lymphocyte_Chemoattractant_BL" 
 [21] "BMP_6"                            "Beta_2_Microglobulin"            
 [23] "Betacellulin"                     "C_Reactive_Protein"              
 [25] "CD40"                             "CD5L"                            
 [27] "Calbindin"                        "Calcitonin"                      
 [29] "CgA"                              "Clusterin_Apo_J"                 
 [31] "Complement_3"                     "Complement_Factor_H"             
 [33] "Connective_Tissue_Growth_Factor"  "Cortisol"                        
 [35] "Creatine_Kinase_MB"               "Cystatin_C"                      
 [37] "EGF_R"                            "EN_RAGE"                         
 [39] "ENA_78"                           "Eotaxin_3"                       
 [41] "FAS"                              "FSH_Follicle_Stimulation_Hormon" 
 [43] "Fas_Ligand"                       "Fatty_Acid_Binding_Protein"      
 [45] "Ferritin"                         "Fetuin_A"                        
 [47] "Fibrinogen"                       "GRO_alpha"                       
 [49] "Gamma_Interferon_induced_Monokin" "Glutathione_S_Transferase_alpha" 
 [51] "HB_EGF"                           "HCC_4"                           
 [53] "Hepatocyte_Growth_Factor_HGF"     "I_309"                           
 [55] "ICAM_1"                           "IGF_BP_2"                        
 [57] "IL_11"                            "IL_13"                           
 [59] "IL_16"                            "IL_17E"                          
 [61] "IL_1alpha"                        "IL_3"                            
 [63] "IL_4"                             "IL_5"                            
 [65] "IL_6"                             "IL_6_Receptor"                   
 [67] "IL_7"                             "IL_8"                            
 [69] "IP_10_Inducible_Protein_10"       "IgA"                             
 [71] "Insulin"                          "Kidney_Injury_Molecule_1_KIM_1"  
 [73] "LOX_1"                            "Leptin"                          
 [75] "Lipoprotein_a"                    "MCP_1"                           
 [77] "MCP_2"                            "MIF"                             
 [79] "MIP_1alpha"                       "MIP_1beta"                       
 [81] "MMP_2"                            "MMP_3"                           
 [83] "MMP10"                            "MMP7"                            
 [85] "Myoglobin"                        "NT_proBNP"                       
 [87] "NrCAM"                            "Osteopontin"                     
 [89] "PAI_1"                            "PAPP_A"                          
 [91] "PLGF"                             "PYY"                             
 [93] "Pancreatic_polypeptide"           "Prolactin"                       
 [95] "Prostatic_Acid_Phosphatase"       "Protein_S"                       
 [97] "Pulmonary_and_Activation_Regulat" "RANTES"                          
 [99] "Resistin"                         "S100b"                           
[101] "SGOT"                             "SHBG"                            
[103] "SOD"                              "Serum_Amyloid_P"                 
[105] "Sortilin"                         "Stem_Cell_Factor"                
[107] "TGF_alpha"                        "TIMP_1"                          
[109] "TNF_RII"                          "TRAIL_R3"                        
[111] "TTR_prealbumin"                   "Tamm_Horsfall_Protein_THP"       
[113] "Thrombomodulin"                   "Thrombopoietin"                  
[115] "Thymus_Expressed_Chemokine_TECK"  "Thyroid_Stimulating_Hormone"     
[117] "Thyroxine_Binding_Globulin"       "Tissue_Factor"                   
[119] "Transferrin"                      "Trefoil_Factor_3_TFF3"           
[121] "VCAM_1"                           "VEGF"                            
[123] "Vitronectin"                      "von_Willebrand_Factor"           
[125] "age"                              "tau"                             
[127] "p_tau"                            "Ab_42"                           
[129] "Genotype"                         "gender"                          
# Base R to accomplish the same:
# table(sapply(X, class))
# Look at what class each predictor has:
X %>% sapply(class) %>% table
.
 factor numeric 
      2     128 
# Pipeline to accomplish the same:
# X %>% sapply(class) %>% .[. == 'factor']
# Identify the factor variables, since they also need special attention:
names(X)[sapply(X, class) == 'factor']
[1] "Genotype" "gender"  

Partial Analysis

We can start by looking at the relationship between some of the more intuitive variables like Age, Gender, and Genotype and impairment, to see if there are any obvious relationships.

Age vs Impairment

data %>% 
  mutate(age_range=cut(age, breaks=5)) %>%
  group_by(age_range, response) %>% tally %>% 
  plot_ly(x=~age_range, y=~n, color=~response, type='bar') %>%
  plotly::layout(hovermode='closest', title='Age vs Impairment')
minimal value for n is 3, returning requested palette with 3 different levels
minimal value for n is 3, returning requested palette with 3 different levels

Genotype vs Impairment

data %>% 
  group_by(Genotype, response) %>% tally %>% 
  plot_ly(x=~Genotype, y=~n, color=~response, type='bar') %>%
  plotly::layout(hovermode='closest', title='Genotype vs Impairment')
minimal value for n is 3, returning requested palette with 3 different levels
minimal value for n is 3, returning requested palette with 3 different levels

Gender vs Impairment

data %>% 
  group_by(gender, response) %>% tally %>% 
  plot_ly(x=~gender, y=~n, color=~response, type='bar') %>%
  layout(hovermode='closest', title='Gender vs Impairment')
minimal value for n is 3, returning requested palette with 3 different levels
minimal value for n is 3, returning requested palette with 3 different levels

Protein Analysis

Now what about the other ~125 variables? We can’t look at them all one at a time so perhaps there is a way to “condense” them into something more manageable:

library(corrplot)
d.pca <- X %>% select(-gender, -Genotype)
cor.mat <- corrplot(cor(d.pca), order='hclust', tl.col='black', tl.cex=.5)

# Extract the variable names from the figure below since they will be useful
# for creating similar plots for comparison (and it's easier to compare in the same order)
cor.var <- rownames(cor.mat)

PCA

# Run PCA, and make sure scaling is on since we didn't do that manually
pca <- prcomp(d.pca, scale=T)

Check how well the Principal Components capture the correlated groups of variables:

data.frame(Cumulative.Variance=summary(pca)$importance[3,]) %>% mutate(PC.Number=1:n()) %>%
  plot_ly(x=~PC.Number, y=~Cumulative.Variance, mode='lines', type='scatter') %>%
  layout(title='Cumulative Variance Explained by Principal Components')

Projections of Features onto PCs

One way to look at how much each PC is related to a feature is to look at the correlation between the feature itself and it’s transformed version in the new Principal Compenent space .. err a simpler way to put that is to say we have some “transformation” (which could be from PCA or anything else), which will assign a new value to every feature for each observation. Then we can just look at how the original values correlate with the transformed ones to see what the transformation is actually doing:

original.data <- d.pca[,cor.var]
pca.data <- pca$x[,1:25] # Using the first 25 PCs only, for brevity
corrplot(cor(original.data, pca.data), tl.col='black', tl.cex=.5)

Applying and Analyzing Predictive Models

Modeling Data Prep

First we have to do a little better of extra massaging of the predictor data to make sure that all the different model types within caret will be able to use it (namely, encoding factors in some numeric way).

# We currently have factors like this:
X %>% select(starts_with('gender'), starts_with('Genotype')) %>% head

We don’t want these to remain factors or a lot of different caret models will choke. Luckily there’s a caret function that makes that pretty easy:

# Here, the dummyVars function will automatically detect and turn factor values into separate columns
X.m <- predict(dummyVars(~., X), X) %>% as.data.frame
X.m %>% select(starts_with('gender'), starts_with('Genotype')) %>% head

Model Helper Functions

These can be ignored for now but are good to come back to for reference on specific configurations and such:

# These functions are mostly just precursors to the following training commands, and
# serve to do things like specify cross validation, how performance measures are 
# calculated, create ensemble models, apply PCA to data subsets, etc.
#
# This isn't a caret thing, it's just my own thing I use to help 
# make it more convenient to save model results on disk in a more 
# convenient way
tr <- proj$getTrainer()
#
# This function with define how performance measures are calculated for each "fold"
summary.function <- function(...){
  arg.list <- list(...)
  if (!('Impaired' %in% names(arg.list[[1]])))
    arg.list[[1]]$Impaired <- arg.list[[1]]$classProb
  c(do.call(twoClassSummary, arg.list), do.call(defaultSummary, arg.list))
}
#
# Training control -- standard caret stuff
tc <- trainControl(
  classProbs=T, method='cv', number=10,
  summaryFunction = summary.function,
  verboseIter=F, savePredictions='final', returnResamp='final', allowParallel=T
)
#
# This is an ensemble specification that will average predictions from 
# multiple models together
get.ensemble.model <- function(tuneList){
  caret.list.args <- list(
    trControl=trainControl(
      method='cv', number=5, classProbs=T,
      summaryFunction = function(...) c(twoClassSummary(...), defaultSummary(...)),
      returnData=F, savePredictions='final',
      allowParallel=T, verboseIter=F
    ),
    tuneList=tuneList
  )
  caret.stack.args <- list(
    method=GetEnsembleAveragingModel(),
    trControl=trainControl(method='none', savePredictions = 'final', classProbs=T,
                           summaryFunction = function(...) c(twoClassSummary(...), defaultSummary(...)),
                           returnResamp='final')
  )
  GetCaretEnsembleModel(caret.list.args, caret.stack.args)
}
#
# Create a vector containing the names of features we do NOT want to preprocess with PCA
non.pca.vars <- X.m %>% select(starts_with('Gender'), starts_with('Genotype'), age) %>% names
#
# Two utility functions for splitting predictor data into separate data frames as well
# as merging them back together before fitting models with the result
pca.split <- function(X) {
  list(
    var=X %>% dplyr::select(one_of(non.pca.vars)),
    pca=X %>% dplyr::select(-one_of(non.pca.vars))
  )
}
pca.combine <- function(pp, X) {
  X <- pca.split(X)
  cbind(X$var, predict(pp, newdata=X$pca))
}

Custom Caret Model Spec

In order to use PCA within the caret modeling framework, you have 3 main options:

  1. Using it’s built in support for preprocessing data with PCA, which is great, but does not at all support only applying PCA to some of the data
  2. Ignore caret, write your own CV loops and results aggregation stuff
  3. Wrap caret model implementations with some functions that do the application of PCA for you

Option 3 is likely the best choice and caret makes it easy to override any part of its built in models (though understanding the consequences of overriding things is sometimes tough). Here is an example of this below:

# Example Caret custom model specification
# PCA preprocessing model wrapper 
# 
# This function will take any valid caret "method" name
# and wrap its fit/predict/prob functions with necessary logic to
# apply PCA to data subsets
get.model <- function(model.name){
  # All Caret models are represented as lists, retrievable using "getModelInfo"
  # Each of these lists has a variety of functions keyed by names like:
  # "fit" - This function takes in data to fit the model on and should return a fit model object
  # "prob" - This function in the list should return predicted probabilities
  # "grid" - This function is responsible for creating hyperparameter grids to train over
  # And several more ...
  model <- getModelInfo()[[model.name]]
  m <- model
  
  # In this case, we're overriding the standard "fit" function with a new function
  # that will split the input data, apply PCA to some of it, and then fit a model
  # on the resulting, smaller dataset
  m$fit <- function(x, y, wts, param, lev, last, classProbs, ...){
    X <- pca.split(x)
    pp <- preProcess(X$pca, method=c('center', 'scale', 'pca'), pcaComp = 40)
    X.train <- cbind(X$var, predict(pp, newdata=X$pca))
    modelFit <- model$fit(X.train, y, wts, param, lev, last, classProbs, ...)
    modelFit$pp <- pp
    modelFit$feature.names <- names(X.train)
    modelFit
  }
  
  # The "predict" and "prob" methods must be overriden as well to apply PCA
  # to any new data given to make predictions on
  m$predict <- function (modelFit, newdata, submodels = NULL) {
    X.test <- pca.combine(modelFit$pp, newdata)
    model$predict(modelFit, X.test, submodels)
  }
  m$prob <- function (modelFit, newdata, submodels = NULL){
    X.test <- pca.combine(modelFit$pp, newdata)
    model$prob(modelFit, X.test, submodels)
  }
  
  # And as is par for the course with Caret, there are always a couple
  # of model types that don't always work as expected when you do custom
  # things like this.  In this case, xgbTree models were using the original
  # feature names to get "importances" of each so that must be overriden
  # here to use the reduced feature list instead
  if (model.name == 'xgbTree'){
    m$varImp = function(object, numTrees = NULL, ...) {
      imp <- xgb.importance(object$feature.names, model = object)
      imp <- as.data.frame(imp)[, 1:2]
      rownames(imp) <- as.character(imp[,1])
      imp <- imp[,2,drop = FALSE]
      colnames(imp) <- "Overall"
      imp   
    }
  }
  m
}

Model Training

With all the precursor stuff in place, the caret models can be called with different tuning settings and in this case, the wrapper function above will also make sure PCA is applied to protein features:

models <- list(
  tr$getModel('pca_glm', method=get.model('glm'), trControl=tc),
  tr$getModel('pca_glmnet', method=get.model('glmnet'), trControl=tc, tuneLength=5),
  tr$getModel('pca_rf', method=get.model('rf'), trControl=tc, tuneGrid=expand.grid(.mtry = c(2,4,8))),
  tr$getModel('pca_rpart', method=get.model('rpart'), tuneLength=10, trControl=tc),
  tr$getModel('pca_gbm', method=get.model('gbm'), tuneLength=5, trControl=tc, verbose=F),
  tr$getModel('pca_xgb', method=get.model('xgbTree'), tuneLength=5, trControl=tc),
  tr$getModel('pca_spline', method=get.model('earth'), preProcess=pre.proc, trControl=tc, tuneLength=5),
  tr$getModel('pca_nnet', method=get.model('nnet'), preProcess=pre.proc, trControl=tc, tuneLength=5, trace=F)
)
names(models) <- sapply(models, function(m) m$name)
# Loop through the models and call "train" on each
pca.results <- lapply(models, function(m) tr$train(m, X.m, y, enable.cache=T)) %>% setNames(names(models))
2016-11-10 08:10:09 INFO::Beginning training for model "pca_glm" (cache name = "model_pca_glm")
2016-11-10 08:10:09 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_pca_glm.Rdata" from disk
2016-11-10 08:10:09 INFO::Training complete for model "pca_glm"
2016-11-10 08:10:09 INFO::Beginning training for model "pca_glmnet" (cache name = "model_pca_glmnet")
2016-11-10 08:10:09 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_pca_glmnet.Rdata" from disk
2016-11-10 08:10:09 INFO::Training complete for model "pca_glmnet"
2016-11-10 08:10:09 INFO::Beginning training for model "pca_rf" (cache name = "model_pca_rf")
2016-11-10 08:10:09 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_pca_rf.Rdata" from disk
2016-11-10 08:10:09 INFO::Training complete for model "pca_rf"
2016-11-10 08:10:09 INFO::Beginning training for model "pca_rpart" (cache name = "model_pca_rpart")
2016-11-10 08:10:09 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_pca_rpart.Rdata" from disk
2016-11-10 08:10:09 INFO::Training complete for model "pca_rpart"
2016-11-10 08:10:09 INFO::Beginning training for model "pca_gbm" (cache name = "model_pca_gbm")
2016-11-10 08:10:09 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_pca_gbm.Rdata" from disk
2016-11-10 08:10:09 INFO::Training complete for model "pca_gbm"
2016-11-10 08:10:09 INFO::Beginning training for model "pca_xgb" (cache name = "model_pca_xgb")
2016-11-10 08:10:09 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_pca_xgb.Rdata" from disk
2016-11-10 08:10:09 INFO::Training complete for model "pca_xgb"
2016-11-10 08:10:09 INFO::Beginning training for model "pca_spline" (cache name = "model_pca_spline")
2016-11-10 08:10:09 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_pca_spline.Rdata" from disk
2016-11-10 08:10:09 INFO::Training complete for model "pca_spline"
2016-11-10 08:10:09 INFO::Beginning training for model "pca_nnet" (cache name = "model_pca_nnet")
2016-11-10 08:10:09 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_pca_nnet.Rdata" from disk
2016-11-10 08:10:09 INFO::Training complete for model "pca_nnet"

Non-PCA Model Training

For comparison, also train a bunch of models on the entire dataset:

library(caretEnsemble)

Attaching package: ‘caretEnsemble’

The following object is masked from ‘package:ggplot2’:

    autoplot
pre.proc <- c('center', 'scale')
# This "ensemble" model will combine predictions from 3 separate other models
ens.model <- list(
  glmnet=caretModelSpec(method='glmnet', preProcess=pre.proc, tuneLength=5),
  gbm=caretModelSpec(method='gbm', tuneLength=5, verbose=F),
  spline=caretModelSpec(method='earth', tuneLength=5, preProcess=pre.proc)
)
models <- list(
  tr$getModel('glm', method='glm', preProcess=pre.proc, trControl=tc),
  tr$getModel('glmnet', method='glmnet', preProcess=pre.proc, trControl=tc, tuneLength=5),
  tr$getModel('nnet', method='nnet', preProcess=pre.proc, trControl=tc, tuneLength=5, trace=F),
  tr$getModel('rpart', method='rpart', tuneLength=10, trControl=tc),
  tr$getModel('gbm', method='gbm', tuneLength=5, trControl=tc, verbose=F),
  tr$getModel('xgb', method='xgbTree', tuneLength=5, trControl=tc),
  tr$getModel('spline', method='earth', preProcess=pre.proc, trControl=tc, tuneLength=5),
  tr$getModel('rf', method='rf', trControl=tc, tuneLength=5),
  tr$getModel('ensemble', method=get.ensemble.model(ens.model), trControl=tc)
)
names(models) <- sapply(models, function(m) m$name)
# Again, loop through the models and run the training process
all.results <- lapply(models, function(m) tr$train(m, X.m, y, enable.cache=T)) %>% setNames(names(models))
2016-11-10 08:10:09 INFO::Beginning training for model "glm" (cache name = "model_glm")
2016-11-10 08:10:09 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_glm.Rdata" from disk
2016-11-10 08:10:09 INFO::Training complete for model "glm"
2016-11-10 08:10:09 INFO::Beginning training for model "glmnet" (cache name = "model_glmnet")
2016-11-10 08:10:09 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_glmnet.Rdata" from disk
2016-11-10 08:10:09 INFO::Training complete for model "glmnet"
2016-11-10 08:10:09 INFO::Beginning training for model "nnet" (cache name = "model_nnet")
2016-11-10 08:10:09 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_nnet.Rdata" from disk
2016-11-10 08:10:09 INFO::Training complete for model "nnet"
2016-11-10 08:10:09 INFO::Beginning training for model "rpart" (cache name = "model_rpart")
2016-11-10 08:10:09 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_rpart.Rdata" from disk
2016-11-10 08:10:10 INFO::Training complete for model "rpart"
2016-11-10 08:10:10 INFO::Beginning training for model "gbm" (cache name = "model_gbm")
2016-11-10 08:10:10 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_gbm.Rdata" from disk
2016-11-10 08:10:10 INFO::Training complete for model "gbm"
2016-11-10 08:10:10 INFO::Beginning training for model "xgb" (cache name = "model_xgb")
2016-11-10 08:10:10 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_xgb.Rdata" from disk
2016-11-10 08:10:10 INFO::Training complete for model "xgb"
2016-11-10 08:10:10 INFO::Beginning training for model "spline" (cache name = "model_spline")
2016-11-10 08:10:10 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_spline.Rdata" from disk
2016-11-10 08:10:10 INFO::Training complete for model "spline"
2016-11-10 08:10:10 INFO::Beginning training for model "rf" (cache name = "model_rf")
2016-11-10 08:10:10 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_rf.Rdata" from disk
2016-11-10 08:10:10 INFO::Training complete for model "rf"
2016-11-10 08:10:10 INFO::Beginning training for model "ensemble" (cache name = "model_ensemble")
2016-11-10 08:10:10 DEBUG::Restoring cached object "~/data/meetups/r_tutorials/tutorial_03/cache/models/model_ensemble.Rdata" from disk
2016-11-10 08:10:10 INFO::Training complete for model "ensemble"

Raw Results

Caret attaches a ton of information to each fit model object:

# The "model" object itself
pca.results[['pca_glm']]$fit$finalModel

Call:  NULL

Coefficients:
  (Intercept)  gender.Female    gender.Male  Genotype.E2E2  Genotype.E2E3  Genotype.E2E4  Genotype.E3E3  
   -6.7028867      1.2825389             NA     12.6556281      1.3260370      3.1554931      1.1988066  
Genotype.E3E4  Genotype.E4E4            age            PC1            PC2            PC3            PC4  
    1.8173419             NA      0.0848972      0.1574130     -0.0532327      1.0802509     -0.0243411  
          PC5            PC6            PC7            PC8            PC9           PC10           PC11  
   -0.4305046     -0.3790200      0.3341016     -0.8673593      0.1649761      0.4377114      0.4745032  
         PC12           PC13           PC14           PC15           PC16           PC17           PC18  
    0.5366593      0.2424596     -0.0186294     -0.0318212     -0.0480346      0.3106117     -0.2465031  
         PC19           PC20           PC21           PC22           PC23           PC24           PC25  
    0.2596782     -0.5054591     -0.4318682     -0.1894219      0.4396659     -0.0002921      0.4673352  
         PC26           PC27           PC28           PC29           PC30           PC31           PC32  
    0.5351138     -0.4806222     -0.1748346      0.2037984     -0.4253246      0.3143912      0.1927480  
         PC33           PC34           PC35           PC36           PC37           PC38           PC39  
   -0.0874630     -0.1230627     -0.4731087     -0.1464317     -0.0408122      0.7974912     -0.0068709  
         PC40  
    0.2124745  

Degrees of Freedom: 332 Total (i.e. Null);  285 Residual
Null Deviance:      390.6 
Residual Deviance: 155.9    AIC: 251.9
# Resampling results for every hyperparameter combination
all.results[['xgb']]$fit$results %>% head

Model Performance

rbind(GetResampleData(pca.results), GetResampleData(all.results)) %>%
  mutate(model=reorder(model, kappa, median)) %>%
  plot_ly(x=~model, y=~kappa, type='box') %>%
  layout(margin=list(b=100), title='Model Performance')
rbind(GetResampleData(pca.results), GetResampleData(all.results)) %>% head
metric <- 'kappa'
dt <- rbind(GetResampleData(pca.results), GetResampleData(all.results)) 
dt.ens <- dt %>% filter(model == 'ensemble') %>% arrange(resample) %>% .[,metric]
dt %>% group_by(model) %>% do({
  d <- .
  d <- d %>% arrange(resample)
  # print(d)
  if (d$model[1] == 'ensemble'){
    data.frame(p=1)
  } else {
    r <- t.test(dt.ens, data.frame(d)[,metric], paired=T, alternative='greater')
    data.frame(p=r$p.value)
  }
}) %>% ungroup %>% arrange(p) %>% 
  mutate(model=reorder(model, p, max)) %>%
  plot_ly(x=~model, y=~p, type='scatter', mode='lines') %>%
  layout(margin=list(b=100,r=100), title='T-Test Statistics')

Feature Importance

var.imp <- GetVarImp(all.results)
Loading required package: nnet
Loading required package: gbm
Loading required package: survival

Attaching package: ‘survival’

The following object is masked from ‘package:caret’:

    cluster

Loading required package: splines
Loaded gbm 2.1.1
Loading required package: earth
Loading required package: plotmo
Loading required package: plotrix
Loading required package: TeachingDemos

Attaching package: ‘TeachingDemos’

The following object is masked from ‘package:plotly’:

    subplot
var.imp %>%
  mutate(feature=reorder(var.imp$feature, var.imp$score, mean)) %>%
  plot_ly(x=~feature, y=~score, color=~model, mode='markers', type='scatter') %>%
  layout(margin=list(b=200), title='Feature Importance (no PCA Features)')
var.model.names <- names(pca.results)[!str_detect(names(pca.results), 'nnet')]
var.imp <- GetVarImp(pca.results[var.model.names])
var.imp %>%
  mutate(feature=reorder(var.imp$feature, var.imp$score, mean)) %>%
  plot_ly(x=~feature, y=~score, color=~model, mode='markers', type='scatter') %>%
  layout(margin=list(b=200), title='Feature Importance (w/ PCA Features)')

Partial Dependence Calculation

pd.vars <- c(
  'age', 'gender.Male', 'gender.Female', 'tau', 
  'Cystatin_C', 'Ab_42', 'Cortisol', 'VEGF',
  'NT_proBNP'
)
pd.models <- c('xgb', 'gbm', 'glmnet', 'spline', 'ensemble')
pred.fun <- function(object, newdata) {
  if ('caretStack' %in% class(object$finalModel)){
    pred <- predict(object$finalModel, newdata=newdata, type='prob')
  } else {
    pred <- predict(object, newdata=newdata, type='prob')
  }
  if (is.vector(pred)) pred
  else pred[,1] 
}
options(error=recover)
registerCores(1) # Increase this to make PD calcs faster
pd.data <- GetPartialDependence(
  all.results[pd.models], pd.vars, pred.fun, 
  X=X.m, # This can come from model objects but only if returnData=T in trainControl
  grid.size=50, grid.window=c(0, 1), # Resize these to better fit range of data
  sample.rate=1, # Decrease this if PD calculations take too long
  verbose=F, seed=SEED
)

Partial Dependence Plotting

First, to help understand where partial dependence comes from it’s helpful to look at the “ICE” (Individual Conditional Expectation) curves. This is a much simpler process than the name makes it sound .. computing these involves nothing more than picking an observation, altering the value of one predictor for that observation, and seeing how the predicted probability from a model changes as that predictor changes:

pd.all <- foreach(pd=pd.data, .combine=rbind)%do%
{ pd$pd %>% dplyr::mutate(predictor=pd$predictor, model=pd$model) }
# Plot the ICE curves for every observation in our dataset (each line represents one observation)
pd.all %>% 
  mutate(i=as.numeric(i)) %>%
  ggplot(aes(x=x, y=y, group=i)) + 
  geom_line(show.legend=F, alpha=.1) + 
  theme_bw() +
  facet_wrap(predictor~model, scales='free', ncol = length(pd.models)) +
  ggtitle('Individual Conditional Expectation Curves (i.e. "Disaggregated" Partial Dependence)')

To make this simpler then, we could simply take the average predicted value across all the observations (i.e. individual lines) to give a single average predicted value for each feature value. This is what partial dependence is:

pd.hist.model <- pd.data[[1]]$model
pd.hist <- lapply(pd.data, function(pd){
  if (pd$model != pd.hist.model) return(NULL)
  else data.frame(pd$x) %>% setNames(pd$predictor)
}) %>% .[!sapply(., is.null)] %>% do.call('cbind', .) %>%
  melt(id.vars=NULL, variable.name='predictor')
pd.mean <- foreach(pd=pd.data, .combine=rbind)%do%
  { pd$pd %>% dplyr::mutate(predictor=pd$predictor, model=pd$model) } %>%
  dplyr::group_by(predictor, model, x) %>% 
  dplyr::summarise(y.mid=mean(y)) %>% ungroup 
ggplot(NULL) + 
  geom_rug(aes(x=value), size=2, alpha=.1, data=pd.hist) +
  geom_line(aes(x=x, y=y.mid, color=model), data=pd.mean) + 
  facet_wrap(~predictor, scale='free') +
  theme_bw() + 
  ylab('Predicted Probability') + xlab('Predictor Value') + 
  ggtitle('Partial Dependence by Model (free Y scale)')

The y-scales above vary widely, so fixing them to be the same gives a much clearer picture of what features are affecting predictions the most:

ggplot(NULL) + 
  geom_rug(aes(x=value), size=2, alpha=.1, data=pd.hist) +
  geom_line(aes(x=x, y=y.mid, color=model), data=pd.mean) + 
  facet_wrap(~predictor, scale='free_x') +
  theme_bw() + 
  ylab('Predicted Probability') + xlab('Predictor Value') + 
  ggtitle('Partial Dependence by Model (fixed Y scale)')

Model Coefficients

glmnet.model <- all.results$glmnet$fit$finalModel
glmnet.coef <- predict(glmnet.model, s=all.results$glmnet$fit$bestTune$lambda, type='coefficients')
glmnet.coef <- glmnet.coef[,1]
glmnet.coef %>% data.frame %>% setNames('Coefficient') %>% add_rownames(var='Feature') %>%
  filter(abs(Coefficient) > 0) %>%
  mutate(Feature=reorder(Feature, Coefficient, mean)) %>%
  plot_ly(x=~Feature, y=~Coefficient, type='bar') %>%
  layout(margin=list(b=200), title='Glmnet Coefficients')

Conclusion

What we’ve done here comes close to inferring similar conclusions as those in the paper that first used the same data (mentioned in the intro). But, taking a lot of the same approaches into the world of business data leaves a lot to be desired IMO. ML models certainly surface useful signals for making predictive conclusions, but the way they do so without any prior knowledge of the problem or the inputs really limits how realistic they can ever be (e.g. the step functions or unbounded relationships are never really true).

A good middle ground is likely frameworks for building models, rather than applying predefined ones (e.g. caret), and hopefully we make such things a topic for another meetup? These kinds of frameworks make it possible to build models that generalize well but also learn realistic relationships, or at least in so far as they could ever be pre-determined.

LS0tCnRpdGxlOiAiUiBOb3RlYm9vayBmb3IgQWx6aGVpbWVycyBBbmFseXNpcyIKYXV0aG9yOiBFcmljIEN6ZWNoCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KClRoaXMgZG9jIHdpbGwgY292ZXIgc29tZSBhcHBsaWNhdGlvbnMgb2Ygc2ltcGxlIGFuZCBjb21wbGV4IG1vZGVscyB0byBvdXIgQWx6aGVpbWVyJ3MgZGF0YXNldCwgYW5kIHRhbGsgYWJvdXQgaG93IHRoZSByZXN1bHRzIGNvdWxkIGJlIGFwcGxpZWQgb3IgaW50ZXJwZXJldGVkIG11Y2ggbGlrZSB0aGUgd29yayBvcmlnaW5hbGx5IGRvbmUgaW4gdGhlIHBhcGVyIFsiTXVsdGlwbGV4ZWQgaW1tdW5vYXNzYXkgcGFuZWwgaWRlbnRpZmllcyBub3ZlbCBDU0YgYmlvbWFya2VycyBmb3IgQWx6aGVpbWVyJ3MgZGlzZWFzZSBkaWFnbm9zaXMgYW5kIHByb2dub3NpcyJdKGh0dHBzOi8vd3d3Lm5jYmkubmxtLm5paC5nb3YvcHVibWVkLzIxNTI2MTk3XSkuCgpTb21lIG9mIHRoZSB0b3BpY3MgY292ZXJlZDoKCi0gSG93IHRvIG1ha2UgY3VzdG9tIENhcmV0IG1vZGVscyAoYW5kIHdoeSB0aGlzIGlzIHVzdWFsbHkgdGhlIGJlc3Qgcm91dGUgdG8gZ28gZm9yIGFueXRoaW5nIG5vbi1zdGFuZGFyZCkKLSBCdWlsZGluZyBlbnNlbWJsZSBtb2RlbHMgd2l0aCBbY2FyZXRFbnNlbWJsZV0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL2NhcmV0RW5zZW1ibGUvdmlnbmV0dGVzL2NhcmV0RW5zZW1ibGUtaW50cm8uaHRtbCkKLSBBIGxpdHRsZSBiaXQgb24gY2xhc3NpZmllciBwZXJmb3JtYW5jZSBtZXRyaWNzIHdoZW4gW0ZhY2luZyBJbWJhbGFuY2VkIERhdGFdKGh0dHA6Ly93d3cucGl0dC5lZHUvfmplZmZjb2huL2JpYmxpby9KZW5pX01ldHJpY3MucGRmKSAoZmlndXJlIDEgb24gcGFnZSA1IG9mIHRoaXMgcGFwZXIgaXMgdGhlIG1vc3QgaW5mb3JtYXRpdmUgdGhpbmcgSSBoYXZlIGV2ZXIgc2VlbiBvbiB0aGlzIHRvcGljKQotIEEgbGl0dGxlIGJpdCBtb3JlIG9uIHBsb3QubHkgYW5kIGl0cyBhd2Vzb21lbmVzcwotIFVuZGVyc3RhbmRpbmcgd2hhdCBibGFjay1ib3ggTUwgbW9kZWxzIGxlYXJuIHdpdGggcGFydGlhbCBkZXBlbmRlbmNlLCBhbmQgd2hlcmUgW1BEUCBjYW4gZmFpbF0oaHR0cHM6Ly9hcnhpdi5vcmcvcGRmLzEzMDkuNjM5MnYyLnBkZikgKHRoZSBmaWd1cmUgb24gcGFnZSA1IGlzIGEgZ3JlYXQgZXhhbXBsZSkKLSBNb2RlbGluZyBwaGlsb3NvcGhpZXMgdy5yLnQuIGxpbmVhciB2cyBub24tbGluZWFyIG1vZGVscwotIEhvdyB0byBmaW5kIGEgbWlkZGxlIGdyb3VuZCBiZXR3ZWVuIGR1bWIsIGxpbmVhciBzdGF0aXN0aWNhbCBtb2RlbHMgdGhhdCBnZW5lcmFsaXplIChpLmUuIG1ha2UgcHJlZGljdGlvbnMpIHBvb3JseSBhbmQgTUwgYWxnb3MgdGhhdCBnZW5lcmFsaXplIHdlbGwgYnV0IG1ha2UgeW91IGZlZWwgZHVtYgoKCmBgYHtyIGluaXQsIHJlc3VsdHM9J2hpZGUnLCB3YXJuaW5nPUYsIG1lc3NhZ2U9RiwgZXJyb3I9RiwgZWNobz1GfQpzb3VyY2UoJ2NvbW1vbi5SJykKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KGNvcnJwbG90KQpsaWJyYXJ5KHN0cmluZ3IpCmxpYnJhcnkocmVzaGFwZTIpCmxpYnJhcnkocGxvdGx5KQpsaWJyYXJ5KGtuaXRyKQpsYXlvdXQgPC0gcGxvdGx5OjpsYXlvdXQKCiMgUGFwZXIgb24gZGF0YToKIyBNdWx0aXBsZXhlZCBJbW11bm9hc3NheSBQYW5lbCBJZGVudGlmaWVzIE5vdmVsIENTRiBCaW9tYXJrZXJzIGZvciBBbHpoZWltZXIncyBEaXNlYXNlIERpYWdub3NpcyBhbmQgUHJvZ25vc2lzCiMgbGlicmFyeShBcHBsaWVkUHJlZGljdGl2ZU1vZGVsaW5nKQojID9kaWFnbm9zaXMKYGBgCgojIFJlZnJlc2hlciBvbiBPdXIgQUQgRGF0YQoKTG9hZCBpbiB0aGUgc2FtZSBkYXRhIGFzIGJlZm9yZSBhbmQgcnVuIHNvbWUgYmFzaWMgYW5hbHlzaXMgdG8gZ2V0IGJhY2sgaW4gdGhlIG11bmdpbmcgbW9vZDoKCmBgYHtyfQojIExvYWQgaW4gdGhlIHdob2xlIEFsemhlaW1lcnMgZGF0YSBmaWxlCmRhdGEgPC0gcmVhZC5jc3YoJ34vcmVwb3MvcG9ydGZvbGlvL2RlbW9uc3RyYXRpdmUvUi9kYXRhc2V0cy9hbHpoZWltZXJzL2FsemhlaW1lcnMuY3N2JykKCiMgUmVtb3ZlIHRoaXMgZmllbGQgLi4uIEkgZm9yZ290IHRvIGRvIHRoYXQgd2hlbiBjcmVhdGluZyB0aGUgZGF0YXNldApkYXRhIDwtIGRhdGEgJT4lIHNlbGVjdCgtbWFsZSkKCiMgTm9ybWFsaXplIGdlbmRlciBsYWJlbHMKc3RvcGlmbm90KGFsbCghaXMubmEoZGF0YSRnZW5kZXIpKSkKbm9ybWFsaXplLmdlbmRlciA8LSBmdW5jdGlvbih4KSB7CiAgaXMubWFsZSA8LSB4ICU+JSB0b2xvd2VyICU+JSBzdHJfZGV0ZWN0KCdebScpCiAgaWZlbHNlKGlzLm1hbGUsICdNYWxlJywgJ0ZlbWFsZScpICU+JSBmYWN0b3IKfQpkYXRhIDwtIGRhdGEgJT4lIG11dGF0ZShnZW5kZXI9bm9ybWFsaXplLmdlbmRlcihnZW5kZXIpKQoKIyBDb252ZXJ0IGludGVnZXIgZmllbGRzIHRvIG51bWVyaWMgZm9yIHRoZSBzYWtlIG9mIGNvbnNpc3RlbmN5CmRhdGEgPC0gZGF0YSAlPiUgbXV0YXRlX2VhY2hfKGZ1bnMoYXMubnVtZXJpYyksIGMoJ0JldGFjZWxsdWxpbicsICdFb3RheGluXzMnKSkKCmhlYWQoZGF0YSkKYGBgCgoKYGBge3J9CiMgUGFyc2Ugb3VyIGRhdGEgaW50byBwcmVkaWN0b3JzIGFuZCBhIGJpbmFyeSByZXNwb25zZSAKIyAoSW1wYWlyZWQgdnMgTm90IEltcGFpcmVkKSBhbmQgY2hlY2sgcmVzcG9uc2UgZnJlcXVlbmN5ClggPC0gZGF0YSAlPiUgc2VsZWN0KC1yZXNwb25zZSkKeSA8LSBkYXRhWywncmVzcG9uc2UnXQp0YWJsZSh5KQpgYGAKCmBgYHtyfQp0YWJsZSh5KSAvIGxlbmd0aCh5KQpgYGAKCmBgYHtyfQojIENoZWNrIG91dCB0aGUgbmFtZXMgb2YgdGhlIHByZWRpY3RvcnMgd2UgaGF2ZToKbmFtZXMoWCkKYGBgCgpgYGB7cn0KIyBCYXNlIFIgdG8gYWNjb21wbGlzaCB0aGUgc2FtZToKIyB0YWJsZShzYXBwbHkoWCwgY2xhc3MpKQoKIyBMb29rIGF0IHdoYXQgY2xhc3MgZWFjaCBwcmVkaWN0b3IgaGFzOgpYICU+JSBzYXBwbHkoY2xhc3MpICU+JSB0YWJsZQpgYGAKCmBgYHtyfQojIFBpcGVsaW5lIHRvIGFjY29tcGxpc2ggdGhlIHNhbWU6CiMgWCAlPiUgc2FwcGx5KGNsYXNzKSAlPiUgLlsuID09ICdmYWN0b3InXQoKIyBJZGVudGlmeSB0aGUgZmFjdG9yIHZhcmlhYmxlcywgc2luY2UgdGhleSBhbHNvIG5lZWQgc3BlY2lhbCBhdHRlbnRpb246Cm5hbWVzKFgpW3NhcHBseShYLCBjbGFzcykgPT0gJ2ZhY3RvciddCmBgYAoKIyBQYXJ0aWFsIEFuYWx5c2lzCgpXZSBjYW4gc3RhcnQgYnkgbG9va2luZyBhdCB0aGUgcmVsYXRpb25zaGlwIGJldHdlZW4gc29tZSBvZiB0aGUgbW9yZSBpbnR1aXRpdmUgdmFyaWFibGVzIGxpa2UgQWdlLCBHZW5kZXIsIGFuZCBHZW5vdHlwZSBhbmQgaW1wYWlybWVudCwgdG8gc2VlIGlmIHRoZXJlIGFyZSBhbnkgb2J2aW91cyByZWxhdGlvbnNoaXBzLgoKIyMgQWdlIHZzIEltcGFpcm1lbnQKCmBgYHtyfQpkYXRhICU+JSAKICBtdXRhdGUoYWdlX3JhbmdlPWN1dChhZ2UsIGJyZWFrcz01KSkgJT4lCiAgZ3JvdXBfYnkoYWdlX3JhbmdlLCByZXNwb25zZSkgJT4lIHRhbGx5ICU+JSAKICBwbG90X2x5KHg9fmFnZV9yYW5nZSwgeT1+biwgY29sb3I9fnJlc3BvbnNlLCB0eXBlPSdiYXInKSAlPiUKICBwbG90bHk6OmxheW91dChob3Zlcm1vZGU9J2Nsb3Nlc3QnLCB0aXRsZT0nQWdlIHZzIEltcGFpcm1lbnQnKQpgYGAKCgojIyBHZW5vdHlwZSB2cyBJbXBhaXJtZW50CgpgYGB7cn0KZGF0YSAlPiUgCiAgZ3JvdXBfYnkoR2Vub3R5cGUsIHJlc3BvbnNlKSAlPiUgdGFsbHkgJT4lIAogIHBsb3RfbHkoeD1+R2Vub3R5cGUsIHk9fm4sIGNvbG9yPX5yZXNwb25zZSwgdHlwZT0nYmFyJykgJT4lCiAgcGxvdGx5OjpsYXlvdXQoaG92ZXJtb2RlPSdjbG9zZXN0JywgdGl0bGU9J0dlbm90eXBlIHZzIEltcGFpcm1lbnQnKQpgYGAKCgojIyBHZW5kZXIgdnMgSW1wYWlybWVudAoKYGBge3J9CmRhdGEgJT4lIAogIGdyb3VwX2J5KGdlbmRlciwgcmVzcG9uc2UpICU+JSB0YWxseSAlPiUgCiAgcGxvdF9seSh4PX5nZW5kZXIsIHk9fm4sIGNvbG9yPX5yZXNwb25zZSwgdHlwZT0nYmFyJykgJT4lCiAgbGF5b3V0KGhvdmVybW9kZT0nY2xvc2VzdCcsIHRpdGxlPSdHZW5kZXIgdnMgSW1wYWlybWVudCcpCmBgYAoKCiMjIyBQcm90ZWluIEFuYWx5c2lzCgpOb3cgd2hhdCBhYm91dCB0aGUgb3RoZXIgfjEyNSB2YXJpYWJsZXM/ICBXZSBjYW4ndCBsb29rIGF0IHRoZW0gYWxsIG9uZSBhdCBhIHRpbWUgc28gcGVyaGFwcyB0aGVyZSBpcyBhIHdheSB0byAiY29uZGVuc2UiIHRoZW0gaW50byBzb21ldGhpbmcgbW9yZSBtYW5hZ2VhYmxlOgoKCmBgYHtyLCBmaWcud2lkdGg9MTAsIGZpZy5oZWlnaHQ9MTB9CmxpYnJhcnkoY29ycnBsb3QpCmQucGNhIDwtIFggJT4lIHNlbGVjdCgtZ2VuZGVyLCAtR2Vub3R5cGUpCmNvci5tYXQgPC0gY29ycnBsb3QoY29yKGQucGNhKSwgb3JkZXI9J2hjbHVzdCcsIHRsLmNvbD0nYmxhY2snLCB0bC5jZXg9LjUpCgojIEV4dHJhY3QgdGhlIHZhcmlhYmxlIG5hbWVzIGZyb20gdGhlIGZpZ3VyZSBiZWxvdyBzaW5jZSB0aGV5IHdpbGwgYmUgdXNlZnVsCiMgZm9yIGNyZWF0aW5nIHNpbWlsYXIgcGxvdHMgZm9yIGNvbXBhcmlzb24gKGFuZCBpdCdzIGVhc2llciB0byBjb21wYXJlIGluIHRoZSBzYW1lIG9yZGVyKQpjb3IudmFyIDwtIHJvd25hbWVzKGNvci5tYXQpCmBgYAoKIyMgUENBIAoKYGBge3J9CiMgUnVuIFBDQSwgYW5kIG1ha2Ugc3VyZSBzY2FsaW5nIGlzIG9uIHNpbmNlIHdlIGRpZG4ndCBkbyB0aGF0IG1hbnVhbGx5CnBjYSA8LSBwcmNvbXAoZC5wY2EsIHNjYWxlPVQpCmBgYAoKCkNoZWNrIGhvdyB3ZWxsIHRoZSBQcmluY2lwYWwgQ29tcG9uZW50cyBjYXB0dXJlIHRoZSBjb3JyZWxhdGVkIGdyb3VwcyBvZiB2YXJpYWJsZXM6CgpgYGB7cn0KZGF0YS5mcmFtZShDdW11bGF0aXZlLlZhcmlhbmNlPXN1bW1hcnkocGNhKSRpbXBvcnRhbmNlWzMsXSkgJT4lIG11dGF0ZShQQy5OdW1iZXI9MTpuKCkpICU+JQogIHBsb3RfbHkoeD1+UEMuTnVtYmVyLCB5PX5DdW11bGF0aXZlLlZhcmlhbmNlLCBtb2RlPSdsaW5lcycsIHR5cGU9J3NjYXR0ZXInKSAlPiUKICBsYXlvdXQodGl0bGU9J0N1bXVsYXRpdmUgVmFyaWFuY2UgRXhwbGFpbmVkIGJ5IFByaW5jaXBhbCBDb21wb25lbnRzJykKYGBgCgoKIyMgUHJvamVjdGlvbnMgb2YgRmVhdHVyZXMgb250byBQQ3MKCk9uZSB3YXkgdG8gbG9vayBhdCBob3cgbXVjaCBlYWNoIFBDIGlzIHJlbGF0ZWQgdG8gYSBmZWF0dXJlIGlzIHRvIGxvb2sgYXQgdGhlIGNvcnJlbGF0aW9uIGJldHdlZW4gdGhlIGZlYXR1cmUgaXRzZWxmIGFuZCBpdCdzIHRyYW5zZm9ybWVkIHZlcnNpb24gaW4gdGhlIG5ldyBQcmluY2lwYWwgQ29tcGVuZW50IHNwYWNlIC4uIGVyciBhIHNpbXBsZXIgd2F5IHRvIHB1dCB0aGF0IGlzIHRvIHNheSB3ZSBoYXZlIHNvbWUgInRyYW5zZm9ybWF0aW9uIiAod2hpY2ggY291bGQgYmUgZnJvbSBQQ0Egb3IgYW55dGhpbmcgZWxzZSksIHdoaWNoIHdpbGwgYXNzaWduIGEgbmV3IHZhbHVlIHRvIGV2ZXJ5IGZlYXR1cmUgZm9yIGVhY2ggb2JzZXJ2YXRpb24uICBUaGVuIHdlIGNhbiBqdXN0IGxvb2sgYXQgaG93IHRoZSBvcmlnaW5hbCB2YWx1ZXMgY29ycmVsYXRlIHdpdGggdGhlIHRyYW5zZm9ybWVkIG9uZXMgdG8gc2VlIHdoYXQgdGhlIHRyYW5zZm9ybWF0aW9uIGlzIGFjdHVhbGx5IGRvaW5nOgoKYGBge3IsIGZpZy53aWR0aD00LCBmaWcuaGVpZ2h0PTEwfQpvcmlnaW5hbC5kYXRhIDwtIGQucGNhWyxjb3IudmFyXQpwY2EuZGF0YSA8LSBwY2EkeFssMToyNV0gIyBVc2luZyB0aGUgZmlyc3QgMjUgUENzIG9ubHksIGZvciBicmV2aXR5CmNvcnJwbG90KGNvcihvcmlnaW5hbC5kYXRhLCBwY2EuZGF0YSksIHRsLmNvbD0nYmxhY2snLCB0bC5jZXg9LjUpCmBgYAoKCiMgQXBwbHlpbmcgYW5kIEFuYWx5emluZyBQcmVkaWN0aXZlIE1vZGVscwoKIyMgTW9kZWxpbmcgRGF0YSBQcmVwCgpGaXJzdCB3ZSBoYXZlIHRvIGRvIGEgbGl0dGxlIGJldHRlciBvZiBleHRyYSBtYXNzYWdpbmcgb2YgdGhlIHByZWRpY3RvciBkYXRhIHRvIG1ha2Ugc3VyZSB0aGF0IGFsbCB0aGUgZGlmZmVyZW50IG1vZGVsIHR5cGVzIHdpdGhpbiBjYXJldCB3aWxsIGJlIGFibGUgdG8gdXNlIGl0IChuYW1lbHksIGVuY29kaW5nIGZhY3RvcnMgaW4gc29tZSBudW1lcmljIHdheSkuCgpgYGB7cn0KIyBXZSBjdXJyZW50bHkgaGF2ZSBmYWN0b3JzIGxpa2UgdGhpczoKWCAlPiUgc2VsZWN0KHN0YXJ0c193aXRoKCdnZW5kZXInKSwgc3RhcnRzX3dpdGgoJ0dlbm90eXBlJykpICU+JSBoZWFkCmBgYAoKV2UgZG9uJ3Qgd2FudCB0aGVzZSB0byByZW1haW4gZmFjdG9ycyBvciBhIGxvdCBvZiBkaWZmZXJlbnQgY2FyZXQgbW9kZWxzIHdpbGwgY2hva2UuICBMdWNraWx5IHRoZXJlJ3MgYSBjYXJldCBmdW5jdGlvbiB0aGF0IG1ha2VzIHRoYXQgcHJldHR5IGVhc3k6CgpgYGB7cn0KIyBIZXJlLCB0aGUgZHVtbXlWYXJzIGZ1bmN0aW9uIHdpbGwgYXV0b21hdGljYWxseSBkZXRlY3QgYW5kIHR1cm4gZmFjdG9yIHZhbHVlcyBpbnRvIHNlcGFyYXRlIGNvbHVtbnMKWC5tIDwtIHByZWRpY3QoZHVtbXlWYXJzKH4uLCBYKSwgWCkgJT4lIGFzLmRhdGEuZnJhbWUKWC5tICU+JSBzZWxlY3Qoc3RhcnRzX3dpdGgoJ2dlbmRlcicpLCBzdGFydHNfd2l0aCgnR2Vub3R5cGUnKSkgJT4lIGhlYWQKYGBgCgoKIyMjIE1vZGVsIEhlbHBlciBGdW5jdGlvbnMKClRoZXNlIGNhbiBiZSBpZ25vcmVkIGZvciBub3cgYnV0IGFyZSBnb29kIHRvIGNvbWUgYmFjayB0byBmb3IgcmVmZXJlbmNlIG9uIHNwZWNpZmljIGNvbmZpZ3VyYXRpb25zIGFuZCBzdWNoOgoKYGBge3J9CiMgVGhlc2UgZnVuY3Rpb25zIGFyZSBtb3N0bHkganVzdCBwcmVjdXJzb3JzIHRvIHRoZSBmb2xsb3dpbmcgdHJhaW5pbmcgY29tbWFuZHMsIGFuZAojIHNlcnZlIHRvIGRvIHRoaW5ncyBsaWtlIHNwZWNpZnkgY3Jvc3MgdmFsaWRhdGlvbiwgaG93IHBlcmZvcm1hbmNlIG1lYXN1cmVzIGFyZSAKIyBjYWxjdWxhdGVkLCBjcmVhdGUgZW5zZW1ibGUgbW9kZWxzLCBhcHBseSBQQ0EgdG8gZGF0YSBzdWJzZXRzLCBldGMuCiMKIyBUaGlzIGlzbid0IGEgY2FyZXQgdGhpbmcsIGl0J3MganVzdCBteSBvd24gdGhpbmcgSSB1c2UgdG8gaGVscCAKIyBtYWtlIGl0IG1vcmUgY29udmVuaWVudCB0byBzYXZlIG1vZGVsIHJlc3VsdHMgb24gZGlzayBpbiBhIG1vcmUgCiMgY29udmVuaWVudCB3YXkKdHIgPC0gcHJvaiRnZXRUcmFpbmVyKCkKIwojIFRoaXMgZnVuY3Rpb24gd2l0aCBkZWZpbmUgaG93IHBlcmZvcm1hbmNlIG1lYXN1cmVzIGFyZSBjYWxjdWxhdGVkIGZvciBlYWNoICJmb2xkIgpzdW1tYXJ5LmZ1bmN0aW9uIDwtIGZ1bmN0aW9uKC4uLil7CiAgYXJnLmxpc3QgPC0gbGlzdCguLi4pCiAgaWYgKCEoJ0ltcGFpcmVkJyAlaW4lIG5hbWVzKGFyZy5saXN0W1sxXV0pKSkKICAgIGFyZy5saXN0W1sxXV0kSW1wYWlyZWQgPC0gYXJnLmxpc3RbWzFdXSRjbGFzc1Byb2IKICBjKGRvLmNhbGwodHdvQ2xhc3NTdW1tYXJ5LCBhcmcubGlzdCksIGRvLmNhbGwoZGVmYXVsdFN1bW1hcnksIGFyZy5saXN0KSkKfQojCiMgVHJhaW5pbmcgY29udHJvbCAtLSBzdGFuZGFyZCBjYXJldCBzdHVmZgp0YyA8LSB0cmFpbkNvbnRyb2woCiAgY2xhc3NQcm9icz1ULCBtZXRob2Q9J2N2JywgbnVtYmVyPTEwLAogIHN1bW1hcnlGdW5jdGlvbiA9IHN1bW1hcnkuZnVuY3Rpb24sCiAgdmVyYm9zZUl0ZXI9Riwgc2F2ZVByZWRpY3Rpb25zPSdmaW5hbCcsIHJldHVyblJlc2FtcD0nZmluYWwnLCBhbGxvd1BhcmFsbGVsPVQKKQojCiMgVGhpcyBpcyBhbiBlbnNlbWJsZSBzcGVjaWZpY2F0aW9uIHRoYXQgd2lsbCBhdmVyYWdlIHByZWRpY3Rpb25zIGZyb20gCiMgbXVsdGlwbGUgbW9kZWxzIHRvZ2V0aGVyCmdldC5lbnNlbWJsZS5tb2RlbCA8LSBmdW5jdGlvbih0dW5lTGlzdCl7CiAgY2FyZXQubGlzdC5hcmdzIDwtIGxpc3QoCiAgICB0ckNvbnRyb2w9dHJhaW5Db250cm9sKAogICAgICBtZXRob2Q9J2N2JywgbnVtYmVyPTUsIGNsYXNzUHJvYnM9VCwKICAgICAgc3VtbWFyeUZ1bmN0aW9uID0gZnVuY3Rpb24oLi4uKSBjKHR3b0NsYXNzU3VtbWFyeSguLi4pLCBkZWZhdWx0U3VtbWFyeSguLi4pKSwKICAgICAgcmV0dXJuRGF0YT1GLCBzYXZlUHJlZGljdGlvbnM9J2ZpbmFsJywKICAgICAgYWxsb3dQYXJhbGxlbD1ULCB2ZXJib3NlSXRlcj1GCiAgICApLAogICAgdHVuZUxpc3Q9dHVuZUxpc3QKICApCiAgY2FyZXQuc3RhY2suYXJncyA8LSBsaXN0KAogICAgbWV0aG9kPUdldEVuc2VtYmxlQXZlcmFnaW5nTW9kZWwoKSwKICAgIHRyQ29udHJvbD10cmFpbkNvbnRyb2wobWV0aG9kPSdub25lJywgc2F2ZVByZWRpY3Rpb25zID0gJ2ZpbmFsJywgY2xhc3NQcm9icz1ULAogICAgICAgICAgICAgICAgICAgICAgICAgICBzdW1tYXJ5RnVuY3Rpb24gPSBmdW5jdGlvbiguLi4pIGModHdvQ2xhc3NTdW1tYXJ5KC4uLiksIGRlZmF1bHRTdW1tYXJ5KC4uLikpLAogICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm5SZXNhbXA9J2ZpbmFsJykKICApCiAgR2V0Q2FyZXRFbnNlbWJsZU1vZGVsKGNhcmV0Lmxpc3QuYXJncywgY2FyZXQuc3RhY2suYXJncykKfQojCiMgQ3JlYXRlIGEgdmVjdG9yIGNvbnRhaW5pbmcgdGhlIG5hbWVzIG9mIGZlYXR1cmVzIHdlIGRvIE5PVCB3YW50IHRvIHByZXByb2Nlc3Mgd2l0aCBQQ0EKbm9uLnBjYS52YXJzIDwtIFgubSAlPiUgc2VsZWN0KHN0YXJ0c193aXRoKCdHZW5kZXInKSwgc3RhcnRzX3dpdGgoJ0dlbm90eXBlJyksIGFnZSkgJT4lIG5hbWVzCiMKIyBUd28gdXRpbGl0eSBmdW5jdGlvbnMgZm9yIHNwbGl0dGluZyBwcmVkaWN0b3IgZGF0YSBpbnRvIHNlcGFyYXRlIGRhdGEgZnJhbWVzIGFzIHdlbGwKIyBhcyBtZXJnaW5nIHRoZW0gYmFjayB0b2dldGhlciBiZWZvcmUgZml0dGluZyBtb2RlbHMgd2l0aCB0aGUgcmVzdWx0CnBjYS5zcGxpdCA8LSBmdW5jdGlvbihYKSB7CiAgbGlzdCgKICAgIHZhcj1YICU+JSBkcGx5cjo6c2VsZWN0KG9uZV9vZihub24ucGNhLnZhcnMpKSwKICAgIHBjYT1YICU+JSBkcGx5cjo6c2VsZWN0KC1vbmVfb2Yobm9uLnBjYS52YXJzKSkKICApCn0KcGNhLmNvbWJpbmUgPC0gZnVuY3Rpb24ocHAsIFgpIHsKICBYIDwtIHBjYS5zcGxpdChYKQogIGNiaW5kKFgkdmFyLCBwcmVkaWN0KHBwLCBuZXdkYXRhPVgkcGNhKSkKfQpgYGAKCiMgQ3VzdG9tIENhcmV0IE1vZGVsIFNwZWMKCkluIG9yZGVyIHRvIHVzZSBQQ0Egd2l0aGluIHRoZSBjYXJldCBtb2RlbGluZyBmcmFtZXdvcmssIHlvdSBoYXZlIDMgbWFpbiBvcHRpb25zOgoKMS4gVXNpbmcgaXQncyBidWlsdCBpbiBzdXBwb3J0IGZvciBwcmVwcm9jZXNzaW5nIGRhdGEgd2l0aCBQQ0EsIHdoaWNoIGlzIGdyZWF0LCBidXQgZG9lcyBub3QgYXQgYWxsIHN1cHBvcnQgb25seSBhcHBseWluZyBQQ0EgdG8gc29tZSBvZiB0aGUgZGF0YQoyLiBJZ25vcmUgY2FyZXQsIHdyaXRlIHlvdXIgb3duIENWIGxvb3BzIGFuZCByZXN1bHRzIGFnZ3JlZ2F0aW9uIHN0dWZmCjMuIFdyYXAgY2FyZXQgbW9kZWwgaW1wbGVtZW50YXRpb25zIHdpdGggc29tZSBmdW5jdGlvbnMgdGhhdCBkbyB0aGUgYXBwbGljYXRpb24gb2YgUENBIGZvciB5b3UKCk9wdGlvbiAzIGlzIGxpa2VseSB0aGUgYmVzdCBjaG9pY2UgYW5kIGNhcmV0IG1ha2VzIGl0IGVhc3kgdG8gb3ZlcnJpZGUgYW55IHBhcnQgb2YgaXRzIGJ1aWx0IGluIG1vZGVscyAodGhvdWdoIHVuZGVyc3RhbmRpbmcgdGhlIGNvbnNlcXVlbmNlcyBvZiBvdmVycmlkaW5nIHRoaW5ncyBpcyBzb21ldGltZXMgdG91Z2gpLiAgSGVyZSBpcyBhbiBleGFtcGxlIG9mIHRoaXMgYmVsb3c6CgpgYGB7cn0KIyBFeGFtcGxlIENhcmV0IGN1c3RvbSBtb2RlbCBzcGVjaWZpY2F0aW9uCgojIFBDQSBwcmVwcm9jZXNzaW5nIG1vZGVsIHdyYXBwZXIgCiMgCiMgVGhpcyBmdW5jdGlvbiB3aWxsIHRha2UgYW55IHZhbGlkIGNhcmV0ICJtZXRob2QiIG5hbWUKIyBhbmQgd3JhcCBpdHMgZml0L3ByZWRpY3QvcHJvYiBmdW5jdGlvbnMgd2l0aCBuZWNlc3NhcnkgbG9naWMgdG8KIyBhcHBseSBQQ0EgdG8gZGF0YSBzdWJzZXRzCmdldC5tb2RlbCA8LSBmdW5jdGlvbihtb2RlbC5uYW1lKXsKICAjIEFsbCBDYXJldCBtb2RlbHMgYXJlIHJlcHJlc2VudGVkIGFzIGxpc3RzLCByZXRyaWV2YWJsZSB1c2luZyAiZ2V0TW9kZWxJbmZvIgogICMgRWFjaCBvZiB0aGVzZSBsaXN0cyBoYXMgYSB2YXJpZXR5IG9mIGZ1bmN0aW9ucyBrZXllZCBieSBuYW1lcyBsaWtlOgogICMgImZpdCIgLSBUaGlzIGZ1bmN0aW9uIHRha2VzIGluIGRhdGEgdG8gZml0IHRoZSBtb2RlbCBvbiBhbmQgc2hvdWxkIHJldHVybiBhIGZpdCBtb2RlbCBvYmplY3QKICAjICJwcm9iIiAtIFRoaXMgZnVuY3Rpb24gaW4gdGhlIGxpc3Qgc2hvdWxkIHJldHVybiBwcmVkaWN0ZWQgcHJvYmFiaWxpdGllcwogICMgImdyaWQiIC0gVGhpcyBmdW5jdGlvbiBpcyByZXNwb25zaWJsZSBmb3IgY3JlYXRpbmcgaHlwZXJwYXJhbWV0ZXIgZ3JpZHMgdG8gdHJhaW4gb3ZlcgogICMgQW5kIHNldmVyYWwgbW9yZSAuLi4KICBtb2RlbCA8LSBnZXRNb2RlbEluZm8oKVtbbW9kZWwubmFtZV1dCiAgbSA8LSBtb2RlbAogIAogICMgSW4gdGhpcyBjYXNlLCB3ZSdyZSBvdmVycmlkaW5nIHRoZSBzdGFuZGFyZCAiZml0IiBmdW5jdGlvbiB3aXRoIGEgbmV3IGZ1bmN0aW9uCiAgIyB0aGF0IHdpbGwgc3BsaXQgdGhlIGlucHV0IGRhdGEsIGFwcGx5IFBDQSB0byBzb21lIG9mIGl0LCBhbmQgdGhlbiBmaXQgYSBtb2RlbAogICMgb24gdGhlIHJlc3VsdGluZywgc21hbGxlciBkYXRhc2V0CiAgbSRmaXQgPC0gZnVuY3Rpb24oeCwgeSwgd3RzLCBwYXJhbSwgbGV2LCBsYXN0LCBjbGFzc1Byb2JzLCAuLi4pewogICAgWCA8LSBwY2Euc3BsaXQoeCkKICAgIHBwIDwtIHByZVByb2Nlc3MoWCRwY2EsIG1ldGhvZD1jKCdjZW50ZXInLCAnc2NhbGUnLCAncGNhJyksIHBjYUNvbXAgPSA0MCkKICAgIFgudHJhaW4gPC0gY2JpbmQoWCR2YXIsIHByZWRpY3QocHAsIG5ld2RhdGE9WCRwY2EpKQogICAgbW9kZWxGaXQgPC0gbW9kZWwkZml0KFgudHJhaW4sIHksIHd0cywgcGFyYW0sIGxldiwgbGFzdCwgY2xhc3NQcm9icywgLi4uKQogICAgbW9kZWxGaXQkcHAgPC0gcHAKICAgIG1vZGVsRml0JGZlYXR1cmUubmFtZXMgPC0gbmFtZXMoWC50cmFpbikKICAgIG1vZGVsRml0CiAgfQogIAogICMgVGhlICJwcmVkaWN0IiBhbmQgInByb2IiIG1ldGhvZHMgbXVzdCBiZSBvdmVycmlkZW4gYXMgd2VsbCB0byBhcHBseSBQQ0EKICAjIHRvIGFueSBuZXcgZGF0YSBnaXZlbiB0byBtYWtlIHByZWRpY3Rpb25zIG9uCiAgbSRwcmVkaWN0IDwtIGZ1bmN0aW9uIChtb2RlbEZpdCwgbmV3ZGF0YSwgc3VibW9kZWxzID0gTlVMTCkgewogICAgWC50ZXN0IDwtIHBjYS5jb21iaW5lKG1vZGVsRml0JHBwLCBuZXdkYXRhKQogICAgbW9kZWwkcHJlZGljdChtb2RlbEZpdCwgWC50ZXN0LCBzdWJtb2RlbHMpCiAgfQogIG0kcHJvYiA8LSBmdW5jdGlvbiAobW9kZWxGaXQsIG5ld2RhdGEsIHN1Ym1vZGVscyA9IE5VTEwpewogICAgWC50ZXN0IDwtIHBjYS5jb21iaW5lKG1vZGVsRml0JHBwLCBuZXdkYXRhKQogICAgbW9kZWwkcHJvYihtb2RlbEZpdCwgWC50ZXN0LCBzdWJtb2RlbHMpCiAgfQogIAogICMgQW5kIGFzIGlzIHBhciBmb3IgdGhlIGNvdXJzZSB3aXRoIENhcmV0LCB0aGVyZSBhcmUgYWx3YXlzIGEgY291cGxlCiAgIyBvZiBtb2RlbCB0eXBlcyB0aGF0IGRvbid0IGFsd2F5cyB3b3JrIGFzIGV4cGVjdGVkIHdoZW4geW91IGRvIGN1c3RvbQogICMgdGhpbmdzIGxpa2UgdGhpcy4gIEluIHRoaXMgY2FzZSwgeGdiVHJlZSBtb2RlbHMgd2VyZSB1c2luZyB0aGUgb3JpZ2luYWwKICAjIGZlYXR1cmUgbmFtZXMgdG8gZ2V0ICJpbXBvcnRhbmNlcyIgb2YgZWFjaCBzbyB0aGF0IG11c3QgYmUgb3ZlcnJpZGVuCiAgIyBoZXJlIHRvIHVzZSB0aGUgcmVkdWNlZCBmZWF0dXJlIGxpc3QgaW5zdGVhZAogIGlmIChtb2RlbC5uYW1lID09ICd4Z2JUcmVlJyl7CiAgICBtJHZhckltcCA9IGZ1bmN0aW9uKG9iamVjdCwgbnVtVHJlZXMgPSBOVUxMLCAuLi4pIHsKICAgICAgaW1wIDwtIHhnYi5pbXBvcnRhbmNlKG9iamVjdCRmZWF0dXJlLm5hbWVzLCBtb2RlbCA9IG9iamVjdCkKICAgICAgaW1wIDwtIGFzLmRhdGEuZnJhbWUoaW1wKVssIDE6Ml0KICAgICAgcm93bmFtZXMoaW1wKSA8LSBhcy5jaGFyYWN0ZXIoaW1wWywxXSkKICAgICAgaW1wIDwtIGltcFssMixkcm9wID0gRkFMU0VdCiAgICAgIGNvbG5hbWVzKGltcCkgPC0gIk92ZXJhbGwiCiAgICAgIGltcCAgIAogICAgfQogIH0KICBtCn0KYGBgCgojIyBNb2RlbCBUcmFpbmluZwoKV2l0aCBhbGwgdGhlIHByZWN1cnNvciBzdHVmZiBpbiBwbGFjZSwgdGhlIGNhcmV0IG1vZGVscyBjYW4gYmUgY2FsbGVkIHdpdGggZGlmZmVyZW50IHR1bmluZyBzZXR0aW5ncyBhbmQgaW4gdGhpcyBjYXNlLCB0aGUgd3JhcHBlciBmdW5jdGlvbiBhYm92ZSB3aWxsIGFsc28gbWFrZSBzdXJlIFBDQSBpcyBhcHBsaWVkIHRvIHByb3RlaW4gZmVhdHVyZXM6CgpgYGB7cn0KbW9kZWxzIDwtIGxpc3QoCiAgdHIkZ2V0TW9kZWwoJ3BjYV9nbG0nLCBtZXRob2Q9Z2V0Lm1vZGVsKCdnbG0nKSwgdHJDb250cm9sPXRjKSwKICB0ciRnZXRNb2RlbCgncGNhX2dsbW5ldCcsIG1ldGhvZD1nZXQubW9kZWwoJ2dsbW5ldCcpLCB0ckNvbnRyb2w9dGMsIHR1bmVMZW5ndGg9NSksCiAgdHIkZ2V0TW9kZWwoJ3BjYV9yZicsIG1ldGhvZD1nZXQubW9kZWwoJ3JmJyksIHRyQ29udHJvbD10YywgdHVuZUdyaWQ9ZXhwYW5kLmdyaWQoLm10cnkgPSBjKDIsNCw4KSkpLAogIHRyJGdldE1vZGVsKCdwY2FfcnBhcnQnLCBtZXRob2Q9Z2V0Lm1vZGVsKCdycGFydCcpLCB0dW5lTGVuZ3RoPTEwLCB0ckNvbnRyb2w9dGMpLAogIHRyJGdldE1vZGVsKCdwY2FfZ2JtJywgbWV0aG9kPWdldC5tb2RlbCgnZ2JtJyksIHR1bmVMZW5ndGg9NSwgdHJDb250cm9sPXRjLCB2ZXJib3NlPUYpLAogIHRyJGdldE1vZGVsKCdwY2FfeGdiJywgbWV0aG9kPWdldC5tb2RlbCgneGdiVHJlZScpLCB0dW5lTGVuZ3RoPTUsIHRyQ29udHJvbD10YyksCiAgdHIkZ2V0TW9kZWwoJ3BjYV9zcGxpbmUnLCBtZXRob2Q9Z2V0Lm1vZGVsKCdlYXJ0aCcpLCBwcmVQcm9jZXNzPXByZS5wcm9jLCB0ckNvbnRyb2w9dGMsIHR1bmVMZW5ndGg9NSksCiAgdHIkZ2V0TW9kZWwoJ3BjYV9ubmV0JywgbWV0aG9kPWdldC5tb2RlbCgnbm5ldCcpLCBwcmVQcm9jZXNzPXByZS5wcm9jLCB0ckNvbnRyb2w9dGMsIHR1bmVMZW5ndGg9NSwgdHJhY2U9RikKKQpuYW1lcyhtb2RlbHMpIDwtIHNhcHBseShtb2RlbHMsIGZ1bmN0aW9uKG0pIG0kbmFtZSkKCiMgTG9vcCB0aHJvdWdoIHRoZSBtb2RlbHMgYW5kIGNhbGwgInRyYWluIiBvbiBlYWNoCnBjYS5yZXN1bHRzIDwtIGxhcHBseShtb2RlbHMsIGZ1bmN0aW9uKG0pIHRyJHRyYWluKG0sIFgubSwgeSwgZW5hYmxlLmNhY2hlPVQpKSAlPiUgc2V0TmFtZXMobmFtZXMobW9kZWxzKSkKYGBgCgojIyBOb24tUENBIE1vZGVsIFRyYWluaW5nCgpGb3IgY29tcGFyaXNvbiwgYWxzbyB0cmFpbiBhIGJ1bmNoIG9mIG1vZGVscyBvbiB0aGUgZW50aXJlIGRhdGFzZXQ6CgpgYGB7cn0KbGlicmFyeShjYXJldEVuc2VtYmxlKQpwcmUucHJvYyA8LSBjKCdjZW50ZXInLCAnc2NhbGUnKQoKIyBUaGlzICJlbnNlbWJsZSIgbW9kZWwgd2lsbCBjb21iaW5lIHByZWRpY3Rpb25zIGZyb20gMyBzZXBhcmF0ZSBvdGhlciBtb2RlbHMKZW5zLm1vZGVsIDwtIGxpc3QoCiAgZ2xtbmV0PWNhcmV0TW9kZWxTcGVjKG1ldGhvZD0nZ2xtbmV0JywgcHJlUHJvY2Vzcz1wcmUucHJvYywgdHVuZUxlbmd0aD01KSwKICBnYm09Y2FyZXRNb2RlbFNwZWMobWV0aG9kPSdnYm0nLCB0dW5lTGVuZ3RoPTUsIHZlcmJvc2U9RiksCiAgc3BsaW5lPWNhcmV0TW9kZWxTcGVjKG1ldGhvZD0nZWFydGgnLCB0dW5lTGVuZ3RoPTUsIHByZVByb2Nlc3M9cHJlLnByb2MpCikKbW9kZWxzIDwtIGxpc3QoCiAgdHIkZ2V0TW9kZWwoJ2dsbScsIG1ldGhvZD0nZ2xtJywgcHJlUHJvY2Vzcz1wcmUucHJvYywgdHJDb250cm9sPXRjKSwKICB0ciRnZXRNb2RlbCgnZ2xtbmV0JywgbWV0aG9kPSdnbG1uZXQnLCBwcmVQcm9jZXNzPXByZS5wcm9jLCB0ckNvbnRyb2w9dGMsIHR1bmVMZW5ndGg9NSksCiAgdHIkZ2V0TW9kZWwoJ25uZXQnLCBtZXRob2Q9J25uZXQnLCBwcmVQcm9jZXNzPXByZS5wcm9jLCB0ckNvbnRyb2w9dGMsIHR1bmVMZW5ndGg9NSwgdHJhY2U9RiksCiAgdHIkZ2V0TW9kZWwoJ3JwYXJ0JywgbWV0aG9kPSdycGFydCcsIHR1bmVMZW5ndGg9MTAsIHRyQ29udHJvbD10YyksCiAgdHIkZ2V0TW9kZWwoJ2dibScsIG1ldGhvZD0nZ2JtJywgdHVuZUxlbmd0aD01LCB0ckNvbnRyb2w9dGMsIHZlcmJvc2U9RiksCiAgdHIkZ2V0TW9kZWwoJ3hnYicsIG1ldGhvZD0neGdiVHJlZScsIHR1bmVMZW5ndGg9NSwgdHJDb250cm9sPXRjKSwKICB0ciRnZXRNb2RlbCgnc3BsaW5lJywgbWV0aG9kPSdlYXJ0aCcsIHByZVByb2Nlc3M9cHJlLnByb2MsIHRyQ29udHJvbD10YywgdHVuZUxlbmd0aD01KSwKICB0ciRnZXRNb2RlbCgncmYnLCBtZXRob2Q9J3JmJywgdHJDb250cm9sPXRjLCB0dW5lTGVuZ3RoPTUpLAogIHRyJGdldE1vZGVsKCdlbnNlbWJsZScsIG1ldGhvZD1nZXQuZW5zZW1ibGUubW9kZWwoZW5zLm1vZGVsKSwgdHJDb250cm9sPXRjKQopCm5hbWVzKG1vZGVscykgPC0gc2FwcGx5KG1vZGVscywgZnVuY3Rpb24obSkgbSRuYW1lKQoKIyBBZ2FpbiwgbG9vcCB0aHJvdWdoIHRoZSBtb2RlbHMgYW5kIHJ1biB0aGUgdHJhaW5pbmcgcHJvY2VzcwphbGwucmVzdWx0cyA8LSBsYXBwbHkobW9kZWxzLCBmdW5jdGlvbihtKSB0ciR0cmFpbihtLCBYLm0sIHksIGVuYWJsZS5jYWNoZT1UKSkgJT4lIHNldE5hbWVzKG5hbWVzKG1vZGVscykpCmBgYAoKIyMgUmF3IFJlc3VsdHMKCkNhcmV0IGF0dGFjaGVzIGEgdG9uIG9mIGluZm9ybWF0aW9uIHRvIGVhY2ggZml0IG1vZGVsIG9iamVjdDoKCmBgYHtyfQojIFRoZSAibW9kZWwiIG9iamVjdCBpdHNlbGYKcGNhLnJlc3VsdHNbWydwY2FfZ2xtJ11dJGZpdCRmaW5hbE1vZGVsCmBgYAoKCmBgYHtyfQojIFJlc2FtcGxpbmcgcmVzdWx0cyBmb3IgZXZlcnkgaHlwZXJwYXJhbWV0ZXIgY29tYmluYXRpb24KYWxsLnJlc3VsdHNbWyd4Z2InXV0kZml0JHJlc3VsdHMgJT4lIGhlYWQKYGBgCgoKIyMgTW9kZWwgUGVyZm9ybWFuY2UKCmBgYHtyfQpyYmluZChHZXRSZXNhbXBsZURhdGEocGNhLnJlc3VsdHMpLCBHZXRSZXNhbXBsZURhdGEoYWxsLnJlc3VsdHMpKSAlPiUKICBtdXRhdGUobW9kZWw9cmVvcmRlcihtb2RlbCwga2FwcGEsIG1lZGlhbikpICU+JQogIHBsb3RfbHkoeD1+bW9kZWwsIHk9fmthcHBhLCB0eXBlPSdib3gnKSAlPiUKICBsYXlvdXQobWFyZ2luPWxpc3QoYj0xMDApLCB0aXRsZT0nTW9kZWwgUGVyZm9ybWFuY2UnKQpgYGAKCgpgYGB7cn0KcmJpbmQoR2V0UmVzYW1wbGVEYXRhKHBjYS5yZXN1bHRzKSwgR2V0UmVzYW1wbGVEYXRhKGFsbC5yZXN1bHRzKSkgJT4lIGhlYWQKYGBgCgpgYGB7cn0KbWV0cmljIDwtICdrYXBwYScKZHQgPC0gcmJpbmQoR2V0UmVzYW1wbGVEYXRhKHBjYS5yZXN1bHRzKSwgR2V0UmVzYW1wbGVEYXRhKGFsbC5yZXN1bHRzKSkgCmR0LmVucyA8LSBkdCAlPiUgZmlsdGVyKG1vZGVsID09ICdlbnNlbWJsZScpICU+JSBhcnJhbmdlKHJlc2FtcGxlKSAlPiUgLlssbWV0cmljXQpkdCAlPiUgZ3JvdXBfYnkobW9kZWwpICU+JSBkbyh7CiAgZCA8LSAuCiAgZCA8LSBkICU+JSBhcnJhbmdlKHJlc2FtcGxlKQogICMgcHJpbnQoZCkKICBpZiAoZCRtb2RlbFsxXSA9PSAnZW5zZW1ibGUnKXsKICAgIGRhdGEuZnJhbWUocD0xKQogIH0gZWxzZSB7CiAgICByIDwtIHQudGVzdChkdC5lbnMsIGRhdGEuZnJhbWUoZClbLG1ldHJpY10sIHBhaXJlZD1ULCBhbHRlcm5hdGl2ZT0nZ3JlYXRlcicpCiAgICBkYXRhLmZyYW1lKHA9ciRwLnZhbHVlKQogIH0KfSkgJT4lIHVuZ3JvdXAgJT4lIGFycmFuZ2UocCkgJT4lIAogIG11dGF0ZShtb2RlbD1yZW9yZGVyKG1vZGVsLCBwLCBtYXgpKSAlPiUKICBwbG90X2x5KHg9fm1vZGVsLCB5PX5wLCB0eXBlPSdzY2F0dGVyJywgbW9kZT0nbGluZXMnKSAlPiUKICBsYXlvdXQobWFyZ2luPWxpc3QoYj0xMDAscj0xMDApLCB0aXRsZT0nVC1UZXN0IFN0YXRpc3RpY3MnKQpgYGAKCiMjIEZlYXR1cmUgSW1wb3J0YW5jZQoKYGBge3J9CnZhci5pbXAgPC0gR2V0VmFySW1wKGFsbC5yZXN1bHRzKQp2YXIuaW1wICU+JQogIG11dGF0ZShmZWF0dXJlPXJlb3JkZXIodmFyLmltcCRmZWF0dXJlLCB2YXIuaW1wJHNjb3JlLCBtZWFuKSkgJT4lCiAgcGxvdF9seSh4PX5mZWF0dXJlLCB5PX5zY29yZSwgY29sb3I9fm1vZGVsLCBtb2RlPSdtYXJrZXJzJywgdHlwZT0nc2NhdHRlcicpICU+JQogIGxheW91dChtYXJnaW49bGlzdChiPTIwMCksIHRpdGxlPSdGZWF0dXJlIEltcG9ydGFuY2UgKG5vIFBDQSBGZWF0dXJlcyknKQpgYGAKCgoKYGBge3J9CnZhci5tb2RlbC5uYW1lcyA8LSBuYW1lcyhwY2EucmVzdWx0cylbIXN0cl9kZXRlY3QobmFtZXMocGNhLnJlc3VsdHMpLCAnbm5ldCcpXQp2YXIuaW1wIDwtIEdldFZhckltcChwY2EucmVzdWx0c1t2YXIubW9kZWwubmFtZXNdKQp2YXIuaW1wICU+JQogIG11dGF0ZShmZWF0dXJlPXJlb3JkZXIodmFyLmltcCRmZWF0dXJlLCB2YXIuaW1wJHNjb3JlLCBtZWFuKSkgJT4lCiAgcGxvdF9seSh4PX5mZWF0dXJlLCB5PX5zY29yZSwgY29sb3I9fm1vZGVsLCBtb2RlPSdtYXJrZXJzJywgdHlwZT0nc2NhdHRlcicpICU+JQogIGxheW91dChtYXJnaW49bGlzdChiPTIwMCksIHRpdGxlPSdGZWF0dXJlIEltcG9ydGFuY2UgKHcvIFBDQSBGZWF0dXJlcyknKQpgYGAKCgo8IS0tIHtyfSAtLT4KPCEtLSAjcGMzIDwtIHJlc3VsdHMkcGNhX3hnYiRmaXQkZmluYWxNb2RlbCRwcCRyb3RhdGlvblssM10gIC0tPgo8IS0tIHNjYWxlX3ZlYyA8LSBmdW5jdGlvbih4KSAoeCAtIG1lZGlhbih4KSkgLyBJUVIoeCkgLS0+CjwhLS0gcmVzdWx0cyRwY2FfeGdiJGZpdCRmaW5hbE1vZGVsJHBwJHJvdGF0aW9uICU+JSB0ICU+JSBhcHBseSgyLCBzY2FsZV92ZWMpICU+JSAgLS0+CjwhLS0gICBhcy5kYXRhLmZyYW1lICU+JSBhZGRfcm93bmFtZXModmFyPSdQQycpICU+JSAgLS0+CjwhLS0gICBwbG90X2x5KHg9fnRhdSwgeT1+QWJfNDIsIHRleHQ9flBDLCB0eXBlPSdzY2F0dGVyJywgbW9kZT0nbWFya2VycycpIC0tPgoKIyMgUGFydGlhbCBEZXBlbmRlbmNlIENhbGN1bGF0aW9uCgpgYGB7cn0KCnBkLnZhcnMgPC0gYygKICAnYWdlJywgJ2dlbmRlci5NYWxlJywgJ2dlbmRlci5GZW1hbGUnLCAndGF1JywgCiAgJ0N5c3RhdGluX0MnLCAnQWJfNDInLCAnQ29ydGlzb2wnLCAnVkVHRicsCiAgJ05UX3Byb0JOUCcKKQpwZC5tb2RlbHMgPC0gYygneGdiJywgJ2dibScsICdnbG1uZXQnLCAnc3BsaW5lJywgJ2Vuc2VtYmxlJykKCnByZWQuZnVuIDwtIGZ1bmN0aW9uKG9iamVjdCwgbmV3ZGF0YSkgewogIGlmICgnY2FyZXRTdGFjaycgJWluJSBjbGFzcyhvYmplY3QkZmluYWxNb2RlbCkpewogICAgcHJlZCA8LSBwcmVkaWN0KG9iamVjdCRmaW5hbE1vZGVsLCBuZXdkYXRhPW5ld2RhdGEsIHR5cGU9J3Byb2InKQogIH0gZWxzZSB7CiAgICBwcmVkIDwtIHByZWRpY3Qob2JqZWN0LCBuZXdkYXRhPW5ld2RhdGEsIHR5cGU9J3Byb2InKQogIH0KICBpZiAoaXMudmVjdG9yKHByZWQpKSBwcmVkCiAgZWxzZSBwcmVkWywxXSAKfQpvcHRpb25zKGVycm9yPXJlY292ZXIpCnJlZ2lzdGVyQ29yZXMoMSkgIyBJbmNyZWFzZSB0aGlzIHRvIG1ha2UgUEQgY2FsY3MgZmFzdGVyCnBkLmRhdGEgPC0gR2V0UGFydGlhbERlcGVuZGVuY2UoCiAgYWxsLnJlc3VsdHNbcGQubW9kZWxzXSwgcGQudmFycywgcHJlZC5mdW4sIAogIFg9WC5tLCAjIFRoaXMgY2FuIGNvbWUgZnJvbSBtb2RlbCBvYmplY3RzIGJ1dCBvbmx5IGlmIHJldHVybkRhdGE9VCBpbiB0cmFpbkNvbnRyb2wKICBncmlkLnNpemU9NTAsIGdyaWQud2luZG93PWMoMCwgMSksICMgUmVzaXplIHRoZXNlIHRvIGJldHRlciBmaXQgcmFuZ2Ugb2YgZGF0YQogIHNhbXBsZS5yYXRlPTEsICMgRGVjcmVhc2UgdGhpcyBpZiBQRCBjYWxjdWxhdGlvbnMgdGFrZSB0b28gbG9uZwogIHZlcmJvc2U9Riwgc2VlZD1TRUVECikKCmBgYAoKCiMjIFBhcnRpYWwgRGVwZW5kZW5jZSBQbG90dGluZwoKRmlyc3QsIHRvIGhlbHAgdW5kZXJzdGFuZCB3aGVyZSBwYXJ0aWFsIGRlcGVuZGVuY2UgY29tZXMgZnJvbSBpdCdzIGhlbHBmdWwgdG8gbG9vayBhdCB0aGUgIklDRSIgKEluZGl2aWR1YWwgQ29uZGl0aW9uYWwgRXhwZWN0YXRpb24pIGN1cnZlcy4gIFRoaXMgaXMgYSBtdWNoIHNpbXBsZXIgcHJvY2VzcyB0aGFuIHRoZSBuYW1lIG1ha2VzIGl0IHNvdW5kIC4uIGNvbXB1dGluZyB0aGVzZSBpbnZvbHZlcyBub3RoaW5nIG1vcmUgdGhhbiBwaWNraW5nIGFuIG9ic2VydmF0aW9uLCBhbHRlcmluZyB0aGUgdmFsdWUgb2Ygb25lIHByZWRpY3RvciBmb3IgdGhhdCBvYnNlcnZhdGlvbiwgYW5kIHNlZWluZyBob3cgdGhlIHByZWRpY3RlZCBwcm9iYWJpbGl0eSBmcm9tIGEgbW9kZWwgY2hhbmdlcyBhcyB0aGF0IHByZWRpY3RvciBjaGFuZ2VzOgoKYGBge3IsIGZpZy53aWR0aD04LCBmaWcuaGVpZ2h0PTEwfQpwZC5hbGwgPC0gZm9yZWFjaChwZD1wZC5kYXRhLCAuY29tYmluZT1yYmluZCklZG8lCnsgcGQkcGQgJT4lIGRwbHlyOjptdXRhdGUocHJlZGljdG9yPXBkJHByZWRpY3RvciwgbW9kZWw9cGQkbW9kZWwpIH0KCiMgUGxvdCB0aGUgSUNFIGN1cnZlcyBmb3IgZXZlcnkgb2JzZXJ2YXRpb24gaW4gb3VyIGRhdGFzZXQgKGVhY2ggbGluZSByZXByZXNlbnRzIG9uZSBvYnNlcnZhdGlvbikKcGQuYWxsICU+JSAKICBtdXRhdGUoaT1hcy5udW1lcmljKGkpKSAlPiUKICBnZ3Bsb3QoYWVzKHg9eCwgeT15LCBncm91cD1pKSkgKyAKICBnZW9tX2xpbmUoc2hvdy5sZWdlbmQ9RiwgYWxwaGE9LjEpICsgCiAgdGhlbWVfYncoKSArCiAgZmFjZXRfd3JhcChwcmVkaWN0b3J+bW9kZWwsIHNjYWxlcz0nZnJlZScsIG5jb2wgPSBsZW5ndGgocGQubW9kZWxzKSkgKwogIGdndGl0bGUoJ0luZGl2aWR1YWwgQ29uZGl0aW9uYWwgRXhwZWN0YXRpb24gQ3VydmVzIChpLmUuICJEaXNhZ2dyZWdhdGVkIiBQYXJ0aWFsIERlcGVuZGVuY2UpJykKYGBgCgpUbyBtYWtlIHRoaXMgc2ltcGxlciB0aGVuLCB3ZSBjb3VsZCBzaW1wbHkgdGFrZSB0aGUgYXZlcmFnZSBwcmVkaWN0ZWQgdmFsdWUgYWNyb3NzIGFsbCB0aGUgb2JzZXJ2YXRpb25zIChpLmUuIGluZGl2aWR1YWwgbGluZXMpIHRvIGdpdmUgYSBzaW5nbGUgYXZlcmFnZSBwcmVkaWN0ZWQgdmFsdWUgZm9yIGVhY2ggZmVhdHVyZSB2YWx1ZS4gIFRoaXMgaXMgd2hhdCBwYXJ0aWFsIGRlcGVuZGVuY2UgaXM6CgpgYGB7ciwgZmlnLndpZHRoPTYsIGZpZy5oZWlnaHQ9M30KcGQuaGlzdC5tb2RlbCA8LSBwZC5kYXRhW1sxXV0kbW9kZWwKcGQuaGlzdCA8LSBsYXBwbHkocGQuZGF0YSwgZnVuY3Rpb24ocGQpewogIGlmIChwZCRtb2RlbCAhPSBwZC5oaXN0Lm1vZGVsKSByZXR1cm4oTlVMTCkKICBlbHNlIGRhdGEuZnJhbWUocGQkeCkgJT4lIHNldE5hbWVzKHBkJHByZWRpY3RvcikKfSkgJT4lIC5bIXNhcHBseSguLCBpcy5udWxsKV0gJT4lIGRvLmNhbGwoJ2NiaW5kJywgLikgJT4lCiAgbWVsdChpZC52YXJzPU5VTEwsIHZhcmlhYmxlLm5hbWU9J3ByZWRpY3RvcicpCgpwZC5tZWFuIDwtIGZvcmVhY2gocGQ9cGQuZGF0YSwgLmNvbWJpbmU9cmJpbmQpJWRvJQogIHsgcGQkcGQgJT4lIGRwbHlyOjptdXRhdGUocHJlZGljdG9yPXBkJHByZWRpY3RvciwgbW9kZWw9cGQkbW9kZWwpIH0gJT4lCiAgZHBseXI6Omdyb3VwX2J5KHByZWRpY3RvciwgbW9kZWwsIHgpICU+JSAKICBkcGx5cjo6c3VtbWFyaXNlKHkubWlkPW1lYW4oeSkpICU+JSB1bmdyb3VwIAoKZ2dwbG90KE5VTEwpICsgCiAgZ2VvbV9ydWcoYWVzKHg9dmFsdWUpLCBzaXplPTIsIGFscGhhPS4xLCBkYXRhPXBkLmhpc3QpICsKICBnZW9tX2xpbmUoYWVzKHg9eCwgeT15Lm1pZCwgY29sb3I9bW9kZWwpLCBkYXRhPXBkLm1lYW4pICsgCiAgZmFjZXRfd3JhcCh+cHJlZGljdG9yLCBzY2FsZT0nZnJlZScpICsKICB0aGVtZV9idygpICsgCiAgeWxhYignUHJlZGljdGVkIFByb2JhYmlsaXR5JykgKyB4bGFiKCdQcmVkaWN0b3IgVmFsdWUnKSArIAogIGdndGl0bGUoJ1BhcnRpYWwgRGVwZW5kZW5jZSBieSBNb2RlbCAoZnJlZSBZIHNjYWxlKScpCmBgYAoKVGhlIHktc2NhbGVzIGFib3ZlIHZhcnkgd2lkZWx5LCBzbyBmaXhpbmcgdGhlbSB0byBiZSB0aGUgc2FtZSBnaXZlcyBhIG11Y2ggY2xlYXJlciBwaWN0dXJlIG9mIHdoYXQgZmVhdHVyZXMgYXJlIGFmZmVjdGluZyBwcmVkaWN0aW9ucyB0aGUgbW9zdDoKCmBgYHtyLCBmaWcud2lkdGg9NiwgZmlnLmhlaWdodD0zfQpnZ3Bsb3QoTlVMTCkgKyAKICBnZW9tX3J1ZyhhZXMoeD12YWx1ZSksIHNpemU9MiwgYWxwaGE9LjEsIGRhdGE9cGQuaGlzdCkgKwogIGdlb21fbGluZShhZXMoeD14LCB5PXkubWlkLCBjb2xvcj1tb2RlbCksIGRhdGE9cGQubWVhbikgKyAKICBmYWNldF93cmFwKH5wcmVkaWN0b3IsIHNjYWxlPSdmcmVlX3gnKSArCiAgdGhlbWVfYncoKSArIAogIHlsYWIoJ1ByZWRpY3RlZCBQcm9iYWJpbGl0eScpICsgeGxhYignUHJlZGljdG9yIFZhbHVlJykgKyAKICBnZ3RpdGxlKCdQYXJ0aWFsIERlcGVuZGVuY2UgYnkgTW9kZWwgKGZpeGVkIFkgc2NhbGUpJykKYGBgCgojIyBNb2RlbCBDb2VmZmljaWVudHMKCmBgYHtyfQpnbG1uZXQubW9kZWwgPC0gYWxsLnJlc3VsdHMkZ2xtbmV0JGZpdCRmaW5hbE1vZGVsCmdsbW5ldC5jb2VmIDwtIHByZWRpY3QoZ2xtbmV0Lm1vZGVsLCBzPWFsbC5yZXN1bHRzJGdsbW5ldCRmaXQkYmVzdFR1bmUkbGFtYmRhLCB0eXBlPSdjb2VmZmljaWVudHMnKQpnbG1uZXQuY29lZiA8LSBnbG1uZXQuY29lZlssMV0KZ2xtbmV0LmNvZWYgJT4lIGRhdGEuZnJhbWUgJT4lIHNldE5hbWVzKCdDb2VmZmljaWVudCcpICU+JSBhZGRfcm93bmFtZXModmFyPSdGZWF0dXJlJykgJT4lCiAgZmlsdGVyKGFicyhDb2VmZmljaWVudCkgPiAwKSAlPiUKICBtdXRhdGUoRmVhdHVyZT1yZW9yZGVyKEZlYXR1cmUsIENvZWZmaWNpZW50LCBtZWFuKSkgJT4lCiAgcGxvdF9seSh4PX5GZWF0dXJlLCB5PX5Db2VmZmljaWVudCwgdHlwZT0nYmFyJykgJT4lCiAgbGF5b3V0KG1hcmdpbj1saXN0KGI9MjAwKSwgdGl0bGU9J0dsbW5ldCBDb2VmZmljaWVudHMnKQpgYGAKCiMgQ29uY2x1c2lvbgoKV2hhdCB3ZSd2ZSBkb25lIGhlcmUgY29tZXMgY2xvc2UgdG8gaW5mZXJyaW5nIHNpbWlsYXIgY29uY2x1c2lvbnMgYXMgdGhvc2UgaW4gdGhlIHBhcGVyIHRoYXQgZmlyc3QgdXNlZCB0aGUgc2FtZSBkYXRhIChtZW50aW9uZWQgaW4gdGhlIGludHJvKS4gIEJ1dCwgdGFraW5nIGEgbG90IG9mIHRoZSBzYW1lIGFwcHJvYWNoZXMgaW50byB0aGUgd29ybGQgb2YgYnVzaW5lc3MgZGF0YSBsZWF2ZXMgYSBsb3QgdG8gYmUgZGVzaXJlZCBJTU8uICBNTCBtb2RlbHMgY2VydGFpbmx5IHN1cmZhY2UgdXNlZnVsIHNpZ25hbHMgZm9yIG1ha2luZyBwcmVkaWN0aXZlIGNvbmNsdXNpb25zLCBidXQgdGhlIHdheSB0aGV5IGRvIHNvIHdpdGhvdXQgYW55IHByaW9yIGtub3dsZWRnZSBvZiB0aGUgcHJvYmxlbSBvciB0aGUgaW5wdXRzIHJlYWxseSBsaW1pdHMgaG93IHJlYWxpc3RpYyB0aGV5IGNhbiBldmVyIGJlIChlLmcuIHRoZSBzdGVwIGZ1bmN0aW9ucyBvciB1bmJvdW5kZWQgcmVsYXRpb25zaGlwcyBhcmUgbmV2ZXIgcmVhbGx5IHRydWUpLgoKQSBnb29kIG1pZGRsZSBncm91bmQgaXMgbGlrZWx5IGZyYW1ld29ya3MgZm9yIGJ1aWxkaW5nIG1vZGVscywgcmF0aGVyIHRoYW4gYXBwbHlpbmcgcHJlZGVmaW5lZCBvbmVzIChlLmcuIGNhcmV0KSwgYW5kIGhvcGVmdWxseSB3ZSBtYWtlIHN1Y2ggdGhpbmdzIGEgdG9waWMgZm9yIGFub3RoZXIgbWVldHVwPyAgVGhlc2Uga2luZHMgb2YgZnJhbWV3b3JrcyBtYWtlIGl0IHBvc3NpYmxlIHRvIGJ1aWxkIG1vZGVscyB0aGF0IGdlbmVyYWxpemUgd2VsbCBidXQgYWxzbyBsZWFybiByZWFsaXN0aWMgcmVsYXRpb25zaGlwcywgb3IgYXQgbGVhc3QgaW4gc28gZmFyIGFzIHRoZXkgY291bGQgZXZlciBiZSBwcmUtZGV0ZXJtaW5lZC4KCgoKPCEtLSBHcmF2ZXlhcmQgLS0+CgoKPCEtLSAjIFRoZSBjb2RlIGJlbG93IHdpbGwgcGxvdCB0aGUgUENBIGxvYWRpbmdzIChpZSBwY2Ekcm90YXRpb24pIG1hdHJpeCAtLT4KPCEtLSAjIGRpcmVjdGx5IHJhdGhlciB0aGFuIGxvb2tpbmcgYXQgY29ycmVsYXRpb25zIGJldHdlZW4gb3JpZ2luYWwgYW5kIHRyYW5zZm9ybWVkIHZhcmlhYmxlcyAtLT4KPCEtLSAjICh0aG91Z2ggdGhlc2Ugc2hvdyBhYm91dCB0aGUgc2FtZSB0aGluZykgLS0+Cgo8IS0tCntyLCBmaWcuaGVpZ2h0PTEwLCBmaWcud2lkdGg9Nn0KbGlicmFyeShyZXNoYXBlMikKZHAgPC0gcGNhJHJvdGF0aW9uWywxOjI1XVtjb3IudmFyLF0gJT4lIGFzLmRhdGEuZnJhbWUgJT4lCiAgYWRkX3Jvd25hbWVzKHZhcj0nZmVhdHVyZScpICU+JQogIG1lbHQoaWQudmFycz0nZmVhdHVyZScsIHZhcmlhYmxlLm5hbWU9J3BjJykKZHAkZmVhdHVyZSA8LSBmYWN0b3IoZHAkZmVhdHVyZSwgbGV2ZWxzPXJldihjb3IudmFyKSkKZHAgJT4lIGdncGxvdChhZXMoeD1wYywgeT1mZWF0dXJlLCBmaWxsPXZhbHVlKSkgKyBnZW9tX3RpbGUoKSArCiAgc2NhbGVfZmlsbF9ncmFkaWVudDIobG93PSdyZWQnLCBoaWdoPSdibHVlJywgbWlkPSd3aGl0ZScpCi0tPgoKPCEtLSBQQ0EgKyBEYXRhIFByb2plY3Rpb25zIC0tPgo8IS0tIHtyLCBmaWcud2lkdGg9MTAsIGZpZy5oZWlnaHQ9MTB9IC0tPgo8IS0tIGkucGNhIDwtIGMoMSwyKSAtLT4KPCEtLSBkLnBjYS5wcmVkIDwtIGFzLmRhdGEuZnJhbWUocHJlZGljdChwY2EsIGQucGNhKVssaS5wY2FdKSAlPiUgc2V0TmFtZXMoLiwgYygnUEMxJywgJ1BDMicpKSAtLT4KPCEtLSBkLnBjYS5wcmVkJHJlc3BvbnNlIDwtIHkgLS0+Cgo8IS0tICMgZC5wY2EucHJlZCAlPiUgcGxvdF9seSh4PX5QQzEsIHk9flBDMiwgY29sb3I9fnJlc3BvbnNlLCB0eXBlPSdzY2F0dGVyJywgbW9kZT0nbWFya2VycycpIC0tPgoKPCEtLSAjIFBhcmFtZXRlcnMgZm9yIGF4aXMgd2l0aCBubyBncmlkIGxpbmVzLCB0aWNrcyBvciBsYWJlbHMgLS0+CjwhLS0gZW1wdHkuYXhpcyA8LSBsaXN0KCAtLT4KPCEtLSAgIHRpdGxlID0gJycsIC0tPgo8IS0tICAgemVyb2xpbmUgPSBGQUxTRSwgLS0+CjwhLS0gICBzaG93bGluZSA9IEZBTFNFLCAtLT4KPCEtLSAgIHNob3d0aWNrbGFiZWxzID0gRkFMU0UsIC0tPgo8IS0tICAgdGlja2xlbiA9IDAsIC0tPgo8IS0tICAgc2hvd2dyaWQgPSBGQUxTRSAtLT4KPCEtLSApIC0tPgoKPCEtLSAjIENyZWF0ZSBhIGxpbmUgcGxvdCBvZiBlYWNoIHZhcmlhYmxlIHNob3dpbmcgd2hpY2ggZGlyZWN0aW9uIGl0IG1vdmVzIHdpdGhpbiBvdXIgMkQgc3BhY2UgLS0+CjwhLS0gcDEgPC0gcGxvdF9seSggLS0+CjwhLS0gICAgIGQucGNhLnByZWQsIHg9flBDMiwgeT1+UEMxLCB0eXBlPSdzY2F0dGVyJywgY29sb3I9fnJlc3BvbnNlLCAgLS0+CjwhLS0gICAgIG1vZGU9J21hcmtlcnMnLCBvcGFjaXR5PTEgLS0+CjwhLS0gICApICU+JSAgLS0+CjwhLS0gICBsYXlvdXQoIC0tPgo8IS0tICAgICB4YXhpcz1saXN0KHNob3dncmlkPUYsIHplcm9saW5lPVQpLCAtLT4KPCEtLSAgICAgeWF4aXM9bGlzdChzaG93Z3JpZD1GLCB6ZXJvbGluZT1UKSAtLT4KPCEtLSAgICkgLS0+Cgo8IS0tICMgQ3JlYXRlIGEgaGVhdG1hcCBvZiBpbXBhaXJtZW50IGluY2lkZW5jZSByYXRlIGFjcm9zcyBvdXIgMkQgc3BhY2UgLS0+CjwhLS0gZC5obSA8LSBkLnBjYS5wcmVkICU+JSAgLS0+CjwhLS0gICBtdXRhdGUoUEMxPWFzLmNoYXJhY3RlcihjdXQoUEMxLCBicmVha3M9MykpLCBQQzI9YXMuY2hhcmFjdGVyKGN1dChQQzIsIGJyZWFrcz0zKSkpICU+JSAtLT4KPCEtLSAgIGdyb3VwX2J5KFBDMSwgUEMyKSAlPiUgc3VtbWFyaXNlKFBDVD0xMDAqc3VtKHJlc3BvbnNlID09ICdJbXBhaXJlZCcpL24oKSkgJT4lIC0tPgo8IS0tICAgYWNhc3QoUEMxIH4gUEMyLCB2YWx1ZS52YXI9J1BDVCcpIC0tPgo8IS0tIHAyIDwtIHBsb3RfbHkoej1kLmhtW2MoMywyLDEpLF0sIHR5cGU9J2hlYXRtYXAnLCByZXZlcnNlc2NhbGU9RikgIyU+JSAtLT4KPCEtLSAgICNsYXlvdXQoeGF4aXM9ZW1wdHkuYXhpcywgeWF4aXM9ZW1wdHkuYXhpcykgLS0+Cgo8IS0tICMgT3ZlcmxheSB0aGUgYWJvdmUgcGxvdHMgb24gdG9wIG9mIG9uZSBhbm90aGVyIC0tPgo8IS0tIHN1YnBsb3QocDIsIHAxLCBtYXJnaW49LTEpICU+JSAgLS0+CjwhLS0gICBsYXlvdXQoIC0tPgo8IS0tICAgICBwYXBlcl9iZ2NvbG9yPSdyZ2JhKDAsMCwwLDApJywgcGxvdF9iZ2NvbG9yPSdyZ2JhKDAsMCwwLDApJywgIC0tPgo8IS0tICAgICB3aWR0aD03NTAsIGhlaWdodD01MDAsIC0tPgo8IS0tICAgICB0aXRsZT0nMkQgUHJvamVjdGlvbiBvZiBDb3JyZWxhdGVkIEZlYXR1cmVzIE92ZXJsYXllZCB3LyBJbXBhaXJtZW50IFJhdGVzJyAtLT4KPCEtLSAgICkgLS0+CgoKCjwhLS0gVFNORSBwcm9qZWN0aW9ucyAtLT4KPCEtLSB7cn0gLS0+CjwhLS0gbGlicmFyeSh0c25lKSAtLT4KPCEtLSBkLnRzbmUgPC0gWCAlPiUgc2VsZWN0KC1nZW5kZXIsIC1HZW5vdHlwZSkgLS0+CjwhLS0gc2NhbGVfdmVjIDwtIGZ1bmN0aW9uKHgpICh4IC0gbWVhbih4KSkgLyBzZCh4KSAtLT4KPCEtLSBkLnRzbmUgPC0gZC50c25lICU+JSBtdXRhdGVfZWFjaChmdW5zKHNjYWxlX3ZlYykpIC0tPgo8IS0tIG0udHNuZSA8LSB0c25lKGQudHNuZSkgLS0+CjwhLS0gbS50c25lICU+JSBhcy5kYXRhLmZyYW1lICU+JSBnZ3Bsb3QoYWVzKHg9VjEsIHk9VjIpKSArIGdlb21fcG9pbnQoKSAtLT4KCg==